@akira-tl/forgerelay 0.9.0 → 0.9.2

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,18 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.2] - 2026-09-04
8
+
9
+ ### Added
10
+
11
+ - Added persistent Workspace-owned `workspace.checkpoint` create/list/inspect/delete operations backed by immutable Git-visible working-tree snapshots. Checkpoints preserve stable Workspace identity across restart, close/reopen, managed-worktree backing recreation, and ordinary idle GC; ignored files stay excluded, Relay execution remains owned by the Execution ForgeRelay, Composite use remains explicit-member scoped, and no Core MCP tool was added.
12
+
13
+ ## [0.9.1] - 2026-09-04
14
+
15
+ ### Added
16
+
17
+ - Added the low-frequency `workspace.recovery` Capability for ForgeRelay-managed worktrees. `status` reports the existing recovery model; `repair` can recreate missing active execution backing only from the surviving ForgeRelay managed branch; and Workspace-scoped `cleanup` removes only proven stale worktree registration or an already-integrated closed-Workspace managed branch. Workspace identity and Task state are preserved, active/unique/ambiguous Git state is never force-deleted, Relay executes recovery on the owning Execution ForgeRelay, and Composite use remains explicit-member scoped.
18
+
7
19
  ## [0.9.0] - 2026-09-04
8
20
 
9
21
  ### Added
@@ -45,4 +45,26 @@ Managed worktree finalize:
45
45
 
46
46
  source checkout 可能被用户或其他 Agent 修改。关闭前不要假设 target branch 仍停留在创建 worktree 时的状态;让 ForgeRelay 的 close 检查决定是否可以安全集成。遇到 diverged target、dirty source、忙碌 workspace 或 cleanup warning 时,保留现有 worktree 状态并向用户说明真实失败点,不要自行强制 reset、merge 或删除 worktree。
47
47
 
48
- 如果需要继续工作,复用原 `workspaceId` 或已知 worktree path;不要因为一次关闭失败就创建新的隔离副本。
48
+ 如果 active managed-worktree Workspace 的物理 backing 丢失,先用 `open_workspace action="inspect"` `workspace.recovery` 的 `status` 查看恢复分类。只有 Capability 返回 `recoverable` 时,才考虑 `workspace.recovery { operation: "repair" }`。
49
+
50
+ `workspace.recovery` 的 repair 规则:
51
+
52
+ - 只恢复当前 persistent `workspaceId`,不会创建新的 Workspace identity;
53
+ - 必须从持久化记录中的原 `forgerelay/*` managed branch 恢复,绝不从 target branch 重新创建替代工作;
54
+ - managed branch、target branch、source repository identity、worktree ownership 任一无法证明时返回 `manual-intervention`,不猜测;
55
+ - 若同一 managed branch 已出现另一个 worktree candidate,拒绝自动恢复;
56
+ - recovery backing 创建后必须重新验证 branch、HEAD、Git registration 和工作树健康状态,验证失败时仅回滚 ForgeRelay 本次创建的临时 backing;
57
+ - repair 不 merge、不 rebase、不 reset、不删除 managed branch,也不移动 target branch。
58
+
59
+ `workspace.recovery { operation: "cleanup" }` 只清理当前 persistent Workspace 能证明归属的 Git administrative residue:
60
+
61
+ - backing 已不存在且 stale registration 仍精确对应持久化 managed branch 时,可只移除该 registration;不执行 repository-wide `git worktree prune`;
62
+ - active Workspace 的 managed branch 始终保留,即使 stale registration 已被清掉,避免 cleanup 把仍需恢复的 Workspace 变成不可恢复状态;
63
+ - closed Workspace 的残留 `forgerelay/*` branch 只有在没有任何 worktree registration 或其他 persistent Workspace identity 持有它、source checkout 仍位于持久化 target branch、且 Git 证明该 branch 已完整并入 target 时才允许删除;
64
+ - unique/unmerged commits、branch ownership mismatch、active backing、缺失 target、source/target identity 不可证明等情况返回 `manual-intervention`,不强制删除;
65
+ - cleanup 返回 `cleaned`、`nothing-to-clean` 或 `manual-intervention` 分类;对 active Workspace 成功清理 registration 后同时返回更新后的 recovery projection;
66
+ - cleanup 不 reopen、close、delete Workspace,也不改变 persistent Workspace lifecycle state。
67
+
68
+ Relay Workspace 的 recovery 在 Execution ForgeRelay 上执行;Composite Workspace 必须显式指定实际拥有该 worktree 的 member。不要在 Gateway 或 Composite identity 上自行解释/修改成员 Git 状态。
69
+
70
+ 如果需要继续工作,复用原 `workspaceId`;不要因为 backing 丢失、一次关闭失败或一次恢复失败就创建新的隔离副本来绕过原状态。
@@ -0,0 +1,75 @@
1
+ # Workspace Checkpoints
2
+
3
+ `workspace.checkpoint` provides low-frequency, persistent checkpoints owned by the current filesystem Workspace. Checkpoints are immutable Git-backed snapshots intended for deliberate recovery/history workflows.
4
+
5
+ ## v0.9.2 surface
6
+
7
+ Supported operations are deliberately limited to:
8
+
9
+ - `create` — create a named immutable checkpoint of the current Git-visible working tree.
10
+ - `list` — return bounded checkpoint metadata, newest identity preserved in creation order.
11
+ - `inspect` — return bounded metadata for one checkpoint.
12
+ - `delete` — explicitly delete one checkpoint and its ForgeRelay-owned Git ref.
13
+
14
+ Restore is **not** part of v0.9.2. Do not emulate restore with checkout/reset or other destructive Git commands unless the user separately and explicitly asks for such Git work outside this Capability.
15
+
16
+ ## Create
17
+
18
+ Use:
19
+
20
+ ```json
21
+ {
22
+ "operation": "create",
23
+ "name": "before parser refactor"
24
+ }
25
+ ```
26
+
27
+ A checkpoint snapshots the current Git-visible working-tree content using a private temporary Git index. Creation does not move branch `HEAD`, write project history, modify files, or mutate the real staging index.
28
+
29
+ The snapshot includes tracked files and non-ignored untracked files that Git would normally admit. Git-ignored files are excluded. Consequently Workspace checkpoints are **not a backup mechanism for secrets, ignored configuration, credentials, build caches, or other ignored files**.
30
+
31
+ Checkpoint metadata includes a stable checkpoint id, user-provided name, creation timestamp, immutable snapshot commit id, base `HEAD`, and bounded file/addition/removal counts. It does not return patches or full file contents by default.
32
+
33
+ ## List and inspect
34
+
35
+ Use `list` for discovery:
36
+
37
+ ```json
38
+ {
39
+ "operation": "list",
40
+ "offset": 0,
41
+ "limit": 50
42
+ }
43
+ ```
44
+
45
+ Use `inspect` only after selecting an id:
46
+
47
+ ```json
48
+ {
49
+ "operation": "inspect",
50
+ "checkpointId": "cp_0123456789"
51
+ }
52
+ ```
53
+
54
+ `list` and `inspect` expose bounded metadata only. Checkpoints do not move when `review.changes` advances its independent last-shown baseline.
55
+
56
+ ## Delete
57
+
58
+ Deletion is explicit:
59
+
60
+ ```json
61
+ {
62
+ "operation": "delete",
63
+ "checkpointId": "cp_0123456789"
64
+ }
65
+ ```
66
+
67
+ ForgeRelay verifies that the Workspace-owned Git ref still points at the checkpoint's recorded immutable commit before deleting it. A mismatched or missing ref is treated as inconsistent state rather than silently deleting something else.
68
+
69
+ Ordinary Workspace close, process restart, idle Workspace cache GC, and managed-worktree backing replacement preserve checkpoints. Permanent `close_workspace { action: "delete" }` removes checkpoints owned by that Workspace identity.
70
+
71
+ ## Relay and Composite Workspaces
72
+
73
+ Checkpoint ownership belongs to the Execution Workspace. Through Workspace Relay, checkpoint operations execute on the remote Execution ForgeRelay and persist there.
74
+
75
+ A Composite Workspace does not own filesystem checkpoints itself. Pass an explicit `member` so the operation targets that member's persistent Workspace identity.
@@ -0,0 +1,20 @@
1
+ import { z } from "zod";
2
+ export const workspaceCheckpointInputSchema = z.discriminatedUnion("operation", [
3
+ z.object({
4
+ operation: z.literal("create"),
5
+ name: z.string().trim().min(1).max(120),
6
+ }).strict(),
7
+ z.object({
8
+ operation: z.literal("list"),
9
+ offset: z.number().int().min(0).optional(),
10
+ limit: z.number().int().min(1).max(100).optional(),
11
+ }).strict(),
12
+ z.object({
13
+ operation: z.literal("inspect"),
14
+ checkpointId: z.string().regex(/^cp_[a-f0-9]{10}$/),
15
+ }).strict(),
16
+ z.object({
17
+ operation: z.literal("delete"),
18
+ checkpointId: z.string().regex(/^cp_[a-f0-9]{10}$/),
19
+ }).strict(),
20
+ ]);
@@ -46,6 +46,12 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
46
46
  description: "Persistent Task Lists owned by the current Workspace.",
47
47
  whenToRead: "Read before creating or maintaining Workspace Tasks.",
48
48
  },
49
+ {
50
+ name: "workspace-checkpoints",
51
+ directory: "workspace/workspace-checkpoints",
52
+ description: "Immutable persistent Git-backed checkpoints owned by the current Workspace.",
53
+ whenToRead: "Read before creating, listing, inspecting, or deleting Workspace checkpoints.",
54
+ },
49
55
  {
50
56
  name: "batch-execution",
51
57
  description: "One-call execution of multiple independent ForgeRelay core operations.",
@@ -95,6 +101,8 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
95
101
  "capability-guides.read",
96
102
  "code.intelligence",
97
103
  "workspace.tasks",
104
+ "workspace.recovery",
105
+ "workspace.checkpoint",
98
106
  ];
99
107
  if (config.toolMode !== "codex") {
100
108
  capabilities.push("batch.execute");
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { MAX_CODE_INTELLIGENCE_RESULT_LIMIT, } from "../../../lsp/code-intelligence-types.js";
3
3
  import { batchExecuteInputSchema, } from "../../operations/batch/types.js";
4
+ import { workspaceCheckpointInputSchema, } from "./capabilities/workspace-checkpoint.js";
4
5
  export class CapabilityError extends Error {
5
6
  code;
6
7
  constructor(code, message) {
@@ -190,6 +191,11 @@ export function createCapabilityRegistry(dependencies) {
190
191
  taskId: z.string().min(1),
191
192
  }).strict(),
192
193
  ]);
194
+ const workspaceRecoveryInput = z.discriminatedUnion("operation", [
195
+ z.object({ operation: z.literal("status") }).strict(),
196
+ z.object({ operation: z.literal("repair") }).strict(),
197
+ z.object({ operation: z.literal("cleanup") }).strict(),
198
+ ]);
193
199
  const subagentSessionInput = z.discriminatedUnion("operation", [
194
200
  z.object({
195
201
  operation: z.literal("start"),
@@ -287,6 +293,30 @@ export function createCapabilityRegistry(dependencies) {
287
293
  run: async (input, context, options) => dependencies.codeIntelligence.run(input, context, options),
288
294
  }]
289
295
  : []),
296
+ ...(dependencies.workspaceRecovery
297
+ ? [{
298
+ name: "workspace.recovery",
299
+ description: "Inspect or safely repair the current managed-worktree Workspace when ForgeRelay can prove the surviving managed branch and execution backing ownership.",
300
+ guideName: "managed-worktrees",
301
+ readGuideBeforeFirstUse: true,
302
+ batchPolicy: "unsupported",
303
+ inputSchema: workspaceRecoveryInput,
304
+ availability: (context) => managedWorktreeAvailability(context, dependencies.workspaceRecovery?.available ?? false, dependencies.workspaceRecovery?.unavailableReason),
305
+ run: async (input, context, options) => dependencies.workspaceRecovery.run(input, context, options),
306
+ }]
307
+ : []),
308
+ ...(dependencies.workspaceCheckpoint
309
+ ? [{
310
+ name: "workspace.checkpoint",
311
+ description: "Create, list, inspect, or delete immutable Git-backed checkpoints owned by the current persistent Workspace.",
312
+ guideName: "workspace-checkpoints",
313
+ readGuideBeforeFirstUse: true,
314
+ batchPolicy: "unsupported",
315
+ inputSchema: workspaceCheckpointInputSchema,
316
+ availability: (context) => filesystemWorkspaceAvailability(context, dependencies.workspaceCheckpoint?.available ?? false, dependencies.workspaceCheckpoint?.unavailableReason),
317
+ run: async (input, context, options) => dependencies.workspaceCheckpoint.run(input, context, options),
318
+ }]
319
+ : []),
290
320
  ...(dependencies.workspaceTasks
291
321
  ? [{
292
322
  name: "workspace.tasks",
@@ -360,6 +390,17 @@ function filesystemWorkspaceAvailability(context, available = true, reason) {
360
390
  }
361
391
  return { available, ...(reason ? { reason } : {}) };
362
392
  }
393
+ function managedWorktreeAvailability(context, available = true, reason) {
394
+ if (context.workspaceKind !== "workspace" ||
395
+ context.workspaceMode !== "worktree" ||
396
+ context.workspaceManaged !== true) {
397
+ return {
398
+ available: false,
399
+ reason: "This capability is available only for a ForgeRelay-managed worktree Workspace.",
400
+ };
401
+ }
402
+ return { available, ...(reason ? { reason } : {}) };
403
+ }
363
404
  function requireWorkspaceRoot(context) {
364
405
  if (context.workspaceKind !== "workspace" || !context.workspaceRoot) {
365
406
  throw new CapabilityError("capability_unavailable", "This capability requires a filesystem-backed Workspace.");
@@ -1,3 +1,4 @@
1
+ import { loadCapabilityGuides } from "./capabilities.js";
1
2
  import { CapabilityError } from "./capability-registry.js";
2
3
  import { formatPathForPrompt } from "../../../workspaces/resources/skills.js";
3
4
  export function workspaceHookInvocation(workspace) {
@@ -13,6 +14,8 @@ export function capabilityContextFor(workspace) {
13
14
  workspaceId: workspace.id,
14
15
  workspaceKind: "workspace",
15
16
  workspaceRoot: workspace.root,
17
+ workspaceMode: workspace.mode,
18
+ workspaceManaged: workspace.worktree?.managed ?? false,
16
19
  guides: workspace.capabilityGuides.map((guide) => ({
17
20
  name: guide.name,
18
21
  description: guide.description,
@@ -21,6 +24,20 @@ export function capabilityContextFor(workspace) {
21
24
  })),
22
25
  };
23
26
  }
27
+ export function managedWorktreeRecoveryCapabilityContext(session, config) {
28
+ return {
29
+ workspaceId: session.id,
30
+ workspaceKind: "workspace",
31
+ workspaceMode: session.mode,
32
+ workspaceManaged: session.managed,
33
+ guides: loadCapabilityGuides(config).map((guide) => ({
34
+ name: guide.name,
35
+ description: guide.description,
36
+ whenToRead: guide.whenToRead,
37
+ path: formatPathForPrompt(guide.filePath),
38
+ })),
39
+ };
40
+ }
24
41
  export function compositeCapabilityContext(workspaceId, guides) {
25
42
  return {
26
43
  workspaceId,
@@ -10,7 +10,7 @@ import { formatAgentsPath } from "../../../../workspaces.js";
10
10
  import { markReturnedOutput, processActivityOutcome, processToolResponse, } from "../../../process/runtime.js";
11
11
  import { capabilityActivityAuditRequest, capabilityActivityAuditResult } from "../../../../subagents/sessions/mcp/audit.js";
12
12
  import { activityRelationFor, activityRequestFor, runActivityTool, runActivityToolWithHooks, standardActivityOutcome, } from "../../core/activity-support.js";
13
- import { capabilityContextFor, workspaceHookInvocation } from "../../core/capability-support.js";
13
+ import { capabilityContextFor, managedWorktreeRecoveryCapabilityContext, 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
15
  import { appendAutomaticMutationDiagnostics } from "./mutation-diagnostics.js";
16
16
  export function createOperationRuntime(options) {
@@ -429,6 +429,58 @@ export function createOperationRuntime(options) {
429
429
  },
430
430
  capabilityRun: async (input, context) => {
431
431
  const { workspaceId, name, arguments: capabilityArguments, file } = input;
432
+ if (name === "workspace.recovery") {
433
+ const session = workspaces.getWorkspaceSession(workspaceId);
434
+ if (session.status === "closed") {
435
+ const startedAt = performance.now();
436
+ try {
437
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, managedWorktreeRecoveryCapabilityContext(session, config), {
438
+ nativeFile: file,
439
+ signal: context.signal,
440
+ requestMeta: context.requestMeta,
441
+ sessionId: context.sessionId,
442
+ batch: context.batch,
443
+ });
444
+ const result = {
445
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
446
+ structuredContent: { name, action: "run", result: execution.value },
447
+ };
448
+ logToolCall(config, {
449
+ tool: toolNames.capability,
450
+ workspaceId: session.id,
451
+ capability: name,
452
+ action: "run",
453
+ success: true,
454
+ durationMs: Math.round(performance.now() - startedAt),
455
+ });
456
+ return result;
457
+ }
458
+ catch (error) {
459
+ const capabilityError = error instanceof CapabilityError
460
+ ? error
461
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
462
+ const result = {
463
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
464
+ structuredContent: {
465
+ name,
466
+ action: "run",
467
+ error: { code: capabilityError.code, message: capabilityError.message },
468
+ },
469
+ isError: true,
470
+ };
471
+ logToolCall(config, {
472
+ tool: toolNames.capability,
473
+ workspaceId: session.id,
474
+ capability: name,
475
+ action: "run",
476
+ success: false,
477
+ durationMs: Math.round(performance.now() - startedAt),
478
+ error: capabilityError.message,
479
+ });
480
+ return result;
481
+ }
482
+ }
483
+ }
432
484
  const workspace = workspaces.getWorkspace(workspaceId);
433
485
  let changedPaths = [];
434
486
  return runActivityTool(activityLifecycle, workspace, hostScopeIdFor(context.requestMeta, context.sessionId), toolNames.capability, activityRequestFor(capabilityActivityAuditRequest(input), context), (activityContext) => runToolWithHooks(hooks, {
@@ -438,6 +490,7 @@ export function createOperationRuntime(options) {
438
490
  payload: { name, action: "run" },
439
491
  isFailure: toolResultIsError,
440
492
  changedPaths: () => changedPaths,
493
+ afterCwd: () => workspaces.getWorkspaceSession(workspaceId).root,
441
494
  operation: async () => {
442
495
  const startedAt = performance.now();
443
496
  try {
@@ -15,7 +15,7 @@ const WRITE_TOOL_ANNOTATIONS = {
15
15
  openWorldHint: false,
16
16
  };
17
17
  export function registerWorkspaceAuxiliaryTools(options) {
18
- const { server, config, workspaces, remoteWorkspaces, compositeWorkspaces, compositeTaskGuides, capabilityRegistry, coreOperations, activityLifecycle, hooks, workspaceTasks, taskReminders, activityQueries, compositeActivity, workspacePanelStates, processSessions, reviewCheckpoints, codeIntelligence, resolveExecutionTarget, prepareExecutionContext, hostScopeIdFor, presentExecutionResult, presentSemanticWorkResult, } = options;
18
+ const { server, config, workspaces, remoteWorkspaces, compositeWorkspaces, compositeTaskGuides, capabilityRegistry, coreOperations, activityLifecycle, hooks, workspaceTasks, workspaceCheckpoints, taskReminders, activityQueries, compositeActivity, workspacePanelStates, processSessions, reviewCheckpoints, codeIntelligence, resolveExecutionTarget, prepareExecutionContext, hostScopeIdFor, presentExecutionResult, presentSemanticWorkResult, } = options;
19
19
  registerAppTool(server, "workspace_instruction", {
20
20
  title: "Read Workspace instruction",
21
21
  description: "App-only lazy data source for one instruction file already advertised by the Workspace presentation. Reading through this UI source does not activate nested instructions or create an Activity.",
@@ -399,6 +399,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
399
399
  },
400
400
  payload: { workspaceId: session.id, action: "delete", mode: session.mode },
401
401
  operation: async () => {
402
+ await workspaceCheckpoints.deleteWorkspace(session.id);
402
403
  workspaces.deleteWorkspace(session.id);
403
404
  workspaceTasks.deleteWorkspace(session.id);
404
405
  taskReminders.forget(session.id);
@@ -444,6 +445,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
444
445
  },
445
446
  payload: { workspaceId: session.id, action: "delete", mode: session.mode },
446
447
  operation: async () => {
448
+ await workspaceCheckpoints.deleteWorkspace(session.id);
447
449
  workspaces.deleteWorkspace(session.id);
448
450
  workspaceTasks.deleteWorkspace(session.id);
449
451
  taskReminders.forget(session.id);
@@ -509,6 +511,7 @@ export function registerWorkspaceAuxiliaryTools(options) {
509
511
  }
510
512
  await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
511
513
  if (action === "delete") {
514
+ await workspaceCheckpoints.deleteWorkspace(workspace.id);
512
515
  workspaces.deleteWorkspace(workspace.id);
513
516
  workspaceTasks.deleteWorkspace(workspace.id);
514
517
  taskReminders.forget(workspace.id);
package/dist/server.js CHANGED
@@ -26,6 +26,7 @@ import { shutdownHttpServer } from "./mcp/server/transport/server-shutdown.js";
26
26
  import { formatPathForPrompt } from "./workspaces/resources/skills.js";
27
27
  import { WorkspaceTaskReminderTracker } from "./workspaces/tasks/workspace-task-reminders.js";
28
28
  import { WorkspaceTaskStore } from "./workspaces/tasks/workspace-tasks.js";
29
+ import { WorkspaceCheckpointStore } from "./workspaces/state/workspace-checkpoints.js";
29
30
  import { compactWorkspacePresentation } from "./workspaces/presentation/workspace-presentation.js";
30
31
  import { formatAgentsPath } from "./workspaces.js";
31
32
  import { summarizeSubagentProfile } from "./subagents/profiles.js";
@@ -49,6 +50,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
49
50
  const compositeWorkspaces = options.compositeWorkspaces
50
51
  ?? new CompositeWorkspaceRegistry(config.stateDir);
51
52
  const workspaceTasks = new WorkspaceTaskStore(config.stateDir);
53
+ const workspaceCheckpoints = new WorkspaceCheckpointStore(config.stateDir);
52
54
  const taskReminders = options.taskReminders
53
55
  ?? new WorkspaceTaskReminderTracker(config.taskReminderInterval, workspaceTasks);
54
56
  const compositeTaskGuides = loadCapabilityGuides(config).filter((guide) => guide.name === "workspace-tasks");
@@ -131,6 +133,34 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
131
133
  const capabilityRegistry = createCapabilityRegistry({
132
134
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
133
135
  ...subagentMcp.registryDependencies,
136
+ workspaceRecovery: {
137
+ available: true,
138
+ run: async (input, context) => ({
139
+ value: await workspaces.runManagedWorktreeRecovery(context.workspaceId, input.operation),
140
+ }),
141
+ },
142
+ workspaceCheckpoint: {
143
+ available: true,
144
+ run: async (input, context) => {
145
+ const root = requireCapabilityWorkspaceRoot(context);
146
+ switch (input.operation) {
147
+ case "create":
148
+ return {
149
+ value: {
150
+ workspaceId: context.workspaceId,
151
+ checkpoint: await workspaceCheckpoints.create(context.workspaceId, root, input.name),
152
+ ignoredFilesIncluded: false,
153
+ },
154
+ };
155
+ case "list":
156
+ return { value: await workspaceCheckpoints.list(context.workspaceId, root, input) };
157
+ case "inspect":
158
+ return { value: await workspaceCheckpoints.inspect(context.workspaceId, root, input.checkpointId) };
159
+ case "delete":
160
+ return { value: await workspaceCheckpoints.delete(context.workspaceId, root, input.checkpointId) };
161
+ }
162
+ },
163
+ },
134
164
  workspaceTasks: {
135
165
  available: true,
136
166
  run: async (input, context) => {
@@ -445,7 +475,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
445
475
  });
446
476
  registerWorkspaceAuxiliaryTools({
447
477
  server, config, workspaces, remoteWorkspaces, compositeWorkspaces, compositeTaskGuides, capabilityRegistry,
448
- coreOperations, activityLifecycle, hooks, workspaceTasks, taskReminders, activityQueries, compositeActivity,
478
+ coreOperations, activityLifecycle, hooks, workspaceTasks, workspaceCheckpoints, taskReminders, activityQueries, compositeActivity,
449
479
  workspacePanelStates, processSessions, reviewCheckpoints, codeIntelligence, resolveExecutionTarget,
450
480
  prepareExecutionContext, hostScopeIdFor, presentExecutionResult, presentSemanticWorkResult,
451
481
  });
@@ -228,7 +228,7 @@ async function isAncestor(ancestor, descendant, cwd) {
228
228
  return false;
229
229
  }
230
230
  }
231
- function managedWorktreePath(input) {
231
+ export function managedWorktreePath(input) {
232
232
  const repoName = sanitizePathSegment(basename(input.repoRoot)) || "repo";
233
233
  return join(input.worktreeRoot, `${repoName}-${input.worktreeId}`);
234
234
  }