dsh-git-worktree 0.7.5 → 0.9.0

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 (150) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.en.md +11 -5
  3. package/README.md +11 -5
  4. package/docs/i18n.md +63 -0
  5. package/lib/adapters/files.d.ts +0 -7
  6. package/lib/adapters/files.js +4 -3
  7. package/lib/adapters/files.js.map +1 -1
  8. package/lib/adapters/git.d.ts +0 -10
  9. package/lib/adapters/git.js +92 -18
  10. package/lib/adapters/git.js.map +1 -1
  11. package/lib/adapters/initial-commit.d.ts +16 -0
  12. package/lib/adapters/initial-commit.js +112 -0
  13. package/lib/adapters/initial-commit.js.map +1 -0
  14. package/lib/adapters/lookup.js +21 -0
  15. package/lib/adapters/lookup.js.map +1 -1
  16. package/lib/adapters/registry.d.ts +0 -7
  17. package/lib/adapters/registry.js +2 -1
  18. package/lib/adapters/registry.js.map +1 -1
  19. package/lib/adapters/safe-file.d.ts +0 -7
  20. package/lib/adapters/safe-file.js +5 -4
  21. package/lib/adapters/safe-file.js.map +1 -1
  22. package/lib/client/WorktreeCreateRow.js +5 -2
  23. package/lib/client/WorktreeCreateRow.js.map +1 -1
  24. package/lib/client/WorktreeReviewRow.d.ts +1 -1
  25. package/lib/client/WorktreeReviewRow.js +11 -9
  26. package/lib/client/WorktreeReviewRow.js.map +1 -1
  27. package/lib/client/actions.d.ts +4 -7
  28. package/lib/client/actions.js +17 -13
  29. package/lib/client/actions.js.map +1 -1
  30. package/lib/client/console-remote/adapter.d.ts +2 -1
  31. package/lib/client/console-remote/adapter.js +37 -32
  32. package/lib/client/console-remote/adapter.js.map +1 -1
  33. package/lib/client/console-remote/index.js +6 -2
  34. package/lib/client/console-remote/index.js.map +1 -1
  35. package/lib/client/i18n.d.ts +25 -0
  36. package/lib/client/i18n.js +43 -0
  37. package/lib/client/i18n.js.map +1 -0
  38. package/lib/client/index.d.ts +1 -1
  39. package/lib/client/index.js +17 -4
  40. package/lib/client/index.js.map +1 -1
  41. package/lib/client/model.d.ts +3 -2
  42. package/lib/client/model.js +8 -8
  43. package/lib/client/model.js.map +1 -1
  44. package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +2 -2
  45. package/lib/client/pre-session/PreSessionWorktreeToggle.js +95 -80
  46. package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -1
  47. package/lib/client/pre-session/controller.d.ts +3 -0
  48. package/lib/client/pre-session/controller.js +27 -13
  49. package/lib/client/pre-session/controller.js.map +1 -1
  50. package/lib/client/pre-session/harness-input.d.ts +13 -0
  51. package/lib/client/pre-session/harness-input.js +18 -0
  52. package/lib/client/pre-session/harness-input.js.map +1 -0
  53. package/lib/client/pre-session/index.d.ts +13 -2
  54. package/lib/client/pre-session/index.js +11 -2
  55. package/lib/client/pre-session/index.js.map +1 -1
  56. package/lib/client/review-console/DeliveryProof.js +15 -12
  57. package/lib/client/review-console/DeliveryProof.js.map +1 -1
  58. package/lib/client/review-console/PreflightStatus.js +15 -8
  59. package/lib/client/review-console/PreflightStatus.js.map +1 -1
  60. package/lib/client/review-console/ReviewActions.d.ts +4 -1
  61. package/lib/client/review-console/ReviewActions.js +139 -114
  62. package/lib/client/review-console/ReviewActions.js.map +1 -1
  63. package/lib/client/review-console/ReviewDetailsModal.d.ts +12 -0
  64. package/lib/client/review-console/ReviewDetailsModal.js +56 -0
  65. package/lib/client/review-console/ReviewDetailsModal.js.map +1 -0
  66. package/lib/client/review-console/ReviewIcon.d.ts +35 -0
  67. package/lib/client/review-console/ReviewIcon.js +14 -0
  68. package/lib/client/review-console/ReviewIcon.js.map +1 -0
  69. package/lib/client/review-console/WorktreeReviewPanel.d.ts +3 -7
  70. package/lib/client/review-console/WorktreeReviewPanel.js +20 -82
  71. package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -1
  72. package/lib/client/review-console/recovery-continuation.d.ts +2 -1
  73. package/lib/client/review-console/recovery-continuation.js +12 -8
  74. package/lib/client/review-console/recovery-continuation.js.map +1 -1
  75. package/lib/client/review-console/review-console.styles.js +101 -23
  76. package/lib/client/review-console/review-console.styles.js.map +1 -1
  77. package/lib/client/styles.js +13 -10
  78. package/lib/client/styles.js.map +1 -1
  79. package/lib/client/target-console/TargetStatusAction.js +87 -65
  80. package/lib/client/target-console/TargetStatusAction.js.map +1 -1
  81. package/lib/client/target-console/WorktreeConsoleView.d.ts +2 -1
  82. package/lib/client/target-console/WorktreeConsoleView.js +42 -37
  83. package/lib/client/target-console/WorktreeConsoleView.js.map +1 -1
  84. package/lib/client/target-console/WorktreeManagerModal.js +3 -1
  85. package/lib/client/target-console/WorktreeManagerModal.js.map +1 -1
  86. package/lib/client/target-console/WorktreeReviewStatus.js +45 -88
  87. package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -1
  88. package/lib/client/target-console/current-polling.d.ts +11 -0
  89. package/lib/client/target-console/current-polling.js +104 -0
  90. package/lib/client/target-console/current-polling.js.map +1 -0
  91. package/lib/client/target-console/target-console.styles.js +8 -0
  92. package/lib/client/target-console/target-console.styles.js.map +1 -1
  93. package/lib/client/workspace-sidebar/index.js +8 -3
  94. package/lib/client/workspace-sidebar/index.js.map +1 -1
  95. package/lib/client/workspace-sidebar/model.d.ts +5 -1
  96. package/lib/client/workspace-sidebar/model.js +97 -23
  97. package/lib/client/workspace-sidebar/model.js.map +1 -1
  98. package/lib/client.js +12228 -8928
  99. package/lib/commands.js +32 -35
  100. package/lib/commands.js.map +1 -1
  101. package/lib/console-contract.d.ts +20 -0
  102. package/lib/console-contract.js.map +1 -1
  103. package/lib/console-host/control-plane.d.ts +3 -1
  104. package/lib/console-host/control-plane.js +186 -95
  105. package/lib/console-host/control-plane.js.map +1 -1
  106. package/lib/console-host/errors.js +2 -1
  107. package/lib/console-host/errors.js.map +1 -1
  108. package/lib/console-host/projection.js +3 -1
  109. package/lib/console-host/projection.js.map +1 -1
  110. package/lib/console-host/review-diff.js +6 -5
  111. package/lib/console-host/review-diff.js.map +1 -1
  112. package/lib/console-host/service.d.ts +27 -23
  113. package/lib/console-host/service.js +66 -51
  114. package/lib/console-host/service.js.map +1 -1
  115. package/lib/console-remote/descriptors.js +4 -2
  116. package/lib/console-remote/descriptors.js.map +1 -1
  117. package/lib/console-remote/remote.d.ts +25 -22
  118. package/lib/console-remote/remote.js.map +1 -1
  119. package/lib/console-remote/schemas.d.ts +14 -2
  120. package/lib/console-remote/schemas.js +8 -0
  121. package/lib/console-remote/schemas.js.map +1 -1
  122. package/lib/i18n/client-messages.d.ts +1671 -0
  123. package/lib/i18n/client-messages.js +1468 -0
  124. package/lib/i18n/client-messages.js.map +1 -0
  125. package/lib/i18n/core.d.ts +12 -0
  126. package/lib/i18n/core.js +10 -0
  127. package/lib/i18n/core.js.map +1 -0
  128. package/lib/i18n/host-messages.d.ts +1495 -0
  129. package/lib/i18n/host-messages.js +1485 -0
  130. package/lib/i18n/host-messages.js.map +1 -0
  131. package/lib/i18n/host.d.ts +9 -0
  132. package/lib/i18n/host.js +34 -0
  133. package/lib/i18n/host.js.map +1 -0
  134. package/lib/i18n/transport-messages.d.ts +10 -0
  135. package/lib/i18n/transport-messages.js +11 -0
  136. package/lib/i18n/transport-messages.js.map +1 -0
  137. package/lib/ports.d.ts +15 -0
  138. package/lib/session-checkout-apply.js +93 -92
  139. package/lib/session-checkout-apply.js.map +1 -1
  140. package/lib/session-checkout-module.js +234 -229
  141. package/lib/session-checkout-module.js.map +1 -1
  142. package/lib/session-target-context.js +2 -1
  143. package/lib/session-target-context.js.map +1 -1
  144. package/lib/tools.d.ts +0 -8
  145. package/lib/tools.js +108 -96
  146. package/lib/tools.js.map +1 -1
  147. package/package.json +3 -1
  148. package/scripts/check-publish.mjs +1 -1
  149. package/scripts/dev-dsh-lib.mjs +1 -1
  150. package/scripts/workspace-sidebar-upstream.mjs +1 -1
@@ -1,3 +1,4 @@
1
+ import { hostMessage } from '../i18n/host.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { resolve } from 'node:path';
3
4
  import { consoleFailure, domainError, failure, outcome } from './errors.js';
@@ -6,7 +7,7 @@ import { ReviewDiffStaleError } from './review-diff.js';
6
7
  function recordOf(registry, checkoutId) {
7
8
  const record = registry.read().managedCheckouts[checkoutId];
8
9
  if (record === undefined)
9
- throw domainError('checkout_missing', 'Worktree 记录不存在');
10
+ throw domainError('checkout_missing', hostMessage('theWorktreeRecordDoesNotExist'));
10
11
  return record;
11
12
  }
12
13
  function sidebarTaskState(record) {
@@ -30,14 +31,14 @@ function sidebarTaskState(record) {
30
31
  }
31
32
  function readyReview(record) {
32
33
  if (record.phase !== 'ready' || record.delivery.state !== 'ready_for_review') {
33
- throw domainError('operation_not_allowed', '当前 Worktree 尚未处于可验收状态');
34
+ throw domainError('operation_not_allowed', hostMessage('theCurrentWorktreeIsNotReadyForReview'));
34
35
  }
35
36
  return record.delivery.review;
36
37
  }
37
38
  function previewReview(record) {
38
39
  if (record.phase !== 'ready'
39
40
  || (record.delivery.state !== 'preview_active' && record.delivery.state !== 'preview_detached')) {
40
- throw domainError('preview_not_active', '当前没有等待验收的 Local Preview');
41
+ throw domainError('preview_not_active', hostMessage('thereIsNoLocalPreviewAwaitingAcceptance'));
41
42
  }
42
43
  return record.delivery.review;
43
44
  }
@@ -55,11 +56,11 @@ function applyConflictContinuation(result, reviewId) {
55
56
  const checkoutId = result.target.checkout.id;
56
57
  const revision = result.target.revision;
57
58
  if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(result.localHeadOid)) {
58
- throw domainError('git_error', '冲突恢复缺少有效的 Local HEAD 身份');
59
+ throw domainError('git_error', hostMessage('conflictRecoveryIsMissingAValidLocalHEADIdentity'));
59
60
  }
60
61
  const localHeadOid = result.localHeadOid;
61
62
  if (result.conflictingFiles.length > 500 || !result.conflictingFiles.every(safeConflictFile)) {
62
- throw domainError('git_error', '冲突恢复包含不安全或越界的文件身份');
63
+ throw domainError('git_error', hostMessage('conflictRecoveryContainsUnsafeOrOutOfBoundsFile'));
63
64
  }
64
65
  const conflictingFiles = [...result.conflictingFiles];
65
66
  return {
@@ -74,7 +75,7 @@ function applyConflictContinuation(result, reviewId) {
74
75
  }
75
76
  function preflightFailure(view) {
76
77
  if (view === undefined)
77
- return failure('git_error', '当前 SessionCheckoutModule 不支持验收预检');
78
+ return failure('git_error', hostMessage('theCurrentSessionCheckoutModuleDoesNotSupportAcceptancePreflight'));
78
79
  if (view.status !== 'blocked')
79
80
  return undefined;
80
81
  const code = view.reason === 'stale_isolated' ? 'stale_isolated'
@@ -92,17 +93,17 @@ export function createWorktreeConsoleControlPlane(options) {
92
93
  async function unboundLocalTarget(sessionId) {
93
94
  const session = options.lookup.getSession(sessionId);
94
95
  if (session === undefined)
95
- throw domainError('session_not_found', '当前 Session 不存在');
96
+ throw domainError('session_not_found', hostMessage('theCurrentSessionDoesNotExist'));
96
97
  if (session.projectId === undefined)
97
- throw domainError('project_not_found', '当前 Session 尚未关联项目');
98
+ throw domainError('project_not_found', hostMessage('theCurrentSessionIsNotAssociatedWithAProject'));
98
99
  const project = options.lookup.getProject(session.projectId);
99
100
  if (project === undefined)
100
- throw domainError('project_not_found', '当前 Session 项目不存在');
101
+ throw domainError('project_not_found', hostMessage('theCurrentSessionSProjectDoesNotExist'));
101
102
  if (!options.files.exists(project.root))
102
- throw domainError('project_root_missing', '当前 Session 项目目录不存在');
103
+ throw domainError('project_root_missing', hostMessage('theCurrentSessionSProjectDirectoryDoesNotExist'));
103
104
  const snapshot = await options.git.inspect(project.root);
104
105
  if (snapshot === null)
105
- throw domainError('not_git_repository', '当前 Session 项目不是可用的 Git Worktree');
106
+ throw domainError('not_git_repository', hostMessage('theCurrentSessionSProjectIsNotAnAvailable'));
106
107
  const status = await options.git.status(project.root);
107
108
  return {
108
109
  project: { id: project.id, name: project.name },
@@ -129,7 +130,7 @@ export function createWorktreeConsoleControlPlane(options) {
129
130
  const session = options.lookup.getSession(sessionId);
130
131
  const project = session?.projectId === undefined ? undefined : options.lookup.getProject(session.projectId);
131
132
  if (project === undefined) {
132
- throw domainError('project_mismatch', '当前 Session Workspace 无法证明属于该 Worktree 的原始项目');
133
+ throw domainError('project_mismatch', hostMessage('theCurrentSessionWorkspaceCannotBeProvenToBelong'));
133
134
  }
134
135
  const cleanedOwnerCwd = record.ownerSessionId === sessionId
135
136
  && record.phase === 'discarded'
@@ -139,28 +140,28 @@ export function createWorktreeConsoleControlPlane(options) {
139
140
  if (cleanedOwnerCwd)
140
141
  return;
141
142
  if (!options.files.exists(project.root)) {
142
- throw domainError('project_mismatch', '当前 Session Workspace 无法证明属于该 Worktree 的原始项目');
143
+ throw domainError('project_mismatch', hostMessage('theCurrentSessionWorkspaceCannotBeProvenToBelong'));
143
144
  }
144
145
  const workspaceRoot = await options.files.canonicalize(project.root);
145
146
  if (!sameLocalRoot(workspaceRoot, expectedRoot)) {
146
- throw domainError('project_mismatch', '当前 Session cwd 与 Worktree 授权边界不一致');
147
+ throw domainError('project_mismatch', hostMessage('theCurrentSessionCwdDoesNotMatchTheWorktree'));
147
148
  }
148
149
  }
149
150
  async function authorize(sessionId, checkoutId) {
150
151
  const caller = await options.module.inspect(sessionId);
151
152
  const record = recordOf(options.registry, checkoutId);
152
153
  if (record.ownerSessionId !== sessionId && record.sourceSessionId !== sessionId) {
153
- throw domainError('not_owner', '当前 Session 无权访问该 Worktree');
154
+ throw domainError('not_owner', hostMessage('theCurrentSessionIsNotAllowedToAccessThis'));
154
155
  }
155
156
  if (record.projectId !== caller.project.id) {
156
- throw domainError('project_mismatch', 'Worktree 与当前 Session 项目不一致');
157
+ throw domainError('project_mismatch', hostMessage('worktreeDoesNotMatchTheCurrentSessionSProject'));
157
158
  }
158
159
  const expectedRoot = record.ownerSessionId === sessionId ? record.managedRoot : record.localRoot;
159
160
  await verifyCallerRoot(sessionId, record, expectedRoot);
160
161
  if (record.phase !== 'discarded') {
161
162
  const visible = await options.module.listManagedWorktreesForSession(sessionId, { checkoutId });
162
163
  if (!visible.some(item => item.checkoutId === checkoutId)) {
163
- throw domainError('not_owner', '当前 Session 无权访问该 Worktree');
164
+ throw domainError('not_owner', hostMessage('theCurrentSessionIsNotAllowedToAccessThis'));
164
165
  }
165
166
  }
166
167
  return record;
@@ -172,7 +173,7 @@ export function createWorktreeConsoleControlPlane(options) {
172
173
  }
173
174
  const caller = await callerTarget(sessionId);
174
175
  if (caller.checkout.kind !== 'isolated') {
175
- throw domainError('not_owner', '当前 Session 无权访问该 Worktree');
176
+ throw domainError('not_owner', hostMessage('theCurrentSessionIsNotAllowedToAccessThis'));
176
177
  }
177
178
  const anchor = await authorize(sessionId, caller.checkout.id);
178
179
  const sourceSessionId = anchor.sourceSessionId ?? anchor.ownerSessionId;
@@ -189,10 +190,10 @@ export function createWorktreeConsoleControlPlane(options) {
189
190
  && requested.projectId === anchor.projectId
190
191
  && sameLocalRoot(requested.localRoot, anchor.localRoot);
191
192
  if (!sameLinkedGroup && !exactAcceptanceHolder) {
192
- throw domainError('not_owner', '当前 Session 无权访问该 Worktree');
193
+ throw domainError('not_owner', hostMessage('theCurrentSessionIsNotAllowedToAccessThis'));
193
194
  }
194
195
  if (!ownerSessionAvailable(requested)) {
195
- throw domainError('checkout_missing', '关联 Worktree 的 owner Session 不可用');
196
+ throw domainError('checkout_missing', hostMessage('theAssociatedWorktreeSOwnerSessionIsUnavailable'));
196
197
  }
197
198
  await verifyCallerRoot(requested.ownerSessionId, requested, requested.managedRoot);
198
199
  if (exactAcceptanceHolder) {
@@ -206,7 +207,7 @@ export function createWorktreeConsoleControlPlane(options) {
206
207
  if (currentHolder?.checkoutId !== currentRequested.checkoutId
207
208
  || currentRequested.ownerSessionId !== requested.ownerSessionId
208
209
  || !sameLocalRoot(currentRequested.localRoot, currentAnchor.localRoot))
209
- throw domainError('not_owner', '验收槽位占用者已变化,请重新检查');
210
+ throw domainError('not_owner', hostMessage('theAcceptanceSlotHolderHasChangedCheckAgain'));
210
211
  return {
211
212
  record: currentRequested,
212
213
  linkedRead: true,
@@ -221,7 +222,7 @@ export function createWorktreeConsoleControlPlane(options) {
221
222
  const managedRoot = await options.module.resolveManagedRoot(record.checkoutId);
222
223
  const snapshot = await options.git.inspect(managedRoot);
223
224
  if (snapshot === null)
224
- throw domainError('checkout_mismatch', 'Worktree Git 身份无法验证');
225
+ throw domainError('checkout_mismatch', hostMessage('theWorktreeGitIdentityCannotBeVerified'));
225
226
  const status = await options.git.status(managedRoot);
226
227
  return { managedRoot, snapshot, dirty: status.dirty };
227
228
  }
@@ -286,7 +287,7 @@ export function createWorktreeConsoleControlPlane(options) {
286
287
  || !sameLocalRoot(currentRequested.localRoot, currentAnchor.localRoot)
287
288
  || observed.managedRoot === null
288
289
  || !sameLocalRoot(observed.managedRoot, currentRequested.managedRoot))
289
- throw domainError('not_owner', '验收槽位占用者已变化,请重新检查');
290
+ throw domainError('not_owner', hostMessage('theAcceptanceSlotHolderHasChangedCheckAgain'));
290
291
  projectedRecord = currentRequested;
291
292
  }
292
293
  return projectReviewSlot(projectedRecord, projectDetails(projectedRecord, sessionId, observed.managedRoot, observed.snapshot, observed.dirty, ownerSessionAvailable(access.record), access.linkedRead, observedDeliveryProof));
@@ -310,14 +311,74 @@ export function createWorktreeConsoleControlPlane(options) {
310
311
  })),
311
312
  };
312
313
  }
314
+ const confirmations = new Map();
315
+ const creating = new Set();
316
+ async function localProject(sessionId) {
317
+ const target = await callerTarget(sessionId);
318
+ const session = options.lookup.getSession(sessionId);
319
+ const project = session?.projectId ? options.lookup.getProject(session.projectId) : undefined;
320
+ if (target.checkout.kind !== 'local' || !project || project.id !== target.project.id || !options.files.exists(project.root)) {
321
+ throw domainError('operation_not_allowed', hostMessage('initialStateChanged'));
322
+ }
323
+ return project;
324
+ }
325
+ async function withCreation(sessionId, action) {
326
+ const sessionKey = `session:${sessionId}`;
327
+ if (creating.has(sessionKey))
328
+ throw domainError('operation_not_allowed', hostMessage('initialRepositoryBusy'));
329
+ creating.add(sessionKey);
330
+ let rootKey;
331
+ try {
332
+ const project = await localProject(sessionId);
333
+ const canonical = await options.files.canonicalize(project.root);
334
+ const key = `root:${process.platform === 'win32' ? canonical.toLowerCase() : canonical}`;
335
+ if (creating.has(key))
336
+ throw domainError('operation_not_allowed', hostMessage('initialRepositoryBusy'));
337
+ rootKey = key;
338
+ creating.add(key);
339
+ return await action(project);
340
+ }
341
+ finally {
342
+ creating.delete(sessionKey);
343
+ if (rootKey)
344
+ creating.delete(rootKey);
345
+ }
346
+ }
347
+ async function createTarget(sourceSessionId) {
348
+ const targetSessionId = createTargetSessionId();
349
+ const launch = await options.module.createIsolatedTarget(sourceSessionId, targetSessionId);
350
+ const record = recordOf(options.registry, launch.target.checkout.id);
351
+ const observed = await observe(record);
352
+ if (record.sourceSessionId !== sourceSessionId
353
+ || record.ownerSessionId !== targetSessionId
354
+ || observed.managedRoot !== launch.managedRoot)
355
+ throw domainError('checkout_mismatch', hostMessage('hostIdentityVerificationFailedForTheNewlyCreatedWorktree'));
356
+ return {
357
+ target: projectDetails(record, sourceSessionId, observed.managedRoot, observed.snapshot, observed.dirty, ownerSessionAvailable(record)),
358
+ targetSessionId,
359
+ managedRoot: launch.managedRoot,
360
+ };
361
+ }
313
362
  return {
314
363
  sidebarTopology: () => outcome(async () => {
364
+ const records = Object.values(options.registry.read().managedCheckouts);
365
+ const memberships = await options.lookup.getSidebarMemberships?.(records.map(record => record.managedRoot));
366
+ const membershipByRoot = new Map(memberships?.map(item => [item.managedRoot, item]));
315
367
  const projects = new Map();
316
- for (const record of Object.values(options.registry.read().managedCheckouts)) {
368
+ for (const record of records) {
317
369
  const project = projects.get(record.projectId) ?? {
318
370
  project: { id: record.projectId, name: record.projectName },
319
371
  tasksByOwner: new Map(),
372
+ workspaceIds: new Set(),
373
+ sessionIds: new Set(),
320
374
  };
375
+ // Preserve directory identities from all iterations, not just the
376
+ // newest owner's badge. Cleanup may already have removed its members.
377
+ const membership = membershipByRoot.get(record.managedRoot);
378
+ for (const id of membership?.workspaceIds ?? [])
379
+ project.workspaceIds.add(id);
380
+ for (const id of membership?.sessionIds ?? [])
381
+ project.sessionIds.add(id);
321
382
  const task = {
322
383
  checkoutId: record.checkoutId,
323
384
  ownerSessionId: record.ownerSessionId,
@@ -341,6 +402,12 @@ export function createWorktreeConsoleControlPlane(options) {
341
402
  projects: [...projects.values()].map(project => ({
342
403
  project: project.project,
343
404
  tasks: [...project.tasksByOwner.values()],
405
+ ...(memberships === undefined ? {} : {
406
+ memberships: {
407
+ workspaceIds: [...project.workspaceIds],
408
+ sessionIds: [...project.sessionIds],
409
+ },
410
+ }),
344
411
  })),
345
412
  };
346
413
  }),
@@ -365,7 +432,7 @@ export function createWorktreeConsoleControlPlane(options) {
365
432
  const session = options.lookup.getSession(request.sessionId);
366
433
  const project = session?.projectId === undefined ? undefined : options.lookup.getProject(session.projectId);
367
434
  if (project === undefined || project.id !== caller.project.id || !options.files.exists(project.root)) {
368
- throw domainError('project_mismatch', '当前 Session Workspace 无法证明关联 Worktree 项目');
435
+ throw domainError('project_mismatch', hostMessage('theCurrentSessionWorkspaceCannotBeProvenToBe'));
369
436
  }
370
437
  localRoot = await options.files.canonicalize(project.root);
371
438
  }
@@ -395,37 +462,61 @@ export function createWorktreeConsoleControlPlane(options) {
395
462
  }
396
463
  return { project: { ...caller.project }, worktrees };
397
464
  }),
398
- create: sourceSessionId => outcome(async () => {
399
- const targetSessionId = createTargetSessionId();
400
- const launch = await options.module.createIsolatedTarget(sourceSessionId, targetSessionId);
401
- const record = recordOf(options.registry, launch.target.checkout.id);
402
- const observed = await observe(record);
403
- if (record.sourceSessionId !== sourceSessionId
404
- || record.ownerSessionId !== targetSessionId
405
- || observed.managedRoot !== launch.managedRoot)
406
- throw domainError('checkout_mismatch', '新建 Worktree 的 Host 身份校验失败');
407
- return {
408
- target: projectDetails(record, sourceSessionId, observed.managedRoot, observed.snapshot, observed.dirty, ownerSessionAvailable(record)),
409
- targetSessionId,
410
- managedRoot: launch.managedRoot,
411
- };
465
+ preflightCreate: sourceSessionId => outcome(async () => {
466
+ const project = await localProject(sourceSessionId);
467
+ const check = await options.git.preflightInitialCommit?.(project.root);
468
+ if (!check || check.kind === 'ready')
469
+ return { kind: 'ready' };
470
+ if (check.kind === 'files') {
471
+ confirmations.delete(sourceSessionId);
472
+ return { kind: 'files' };
473
+ }
474
+ for (const [id, value] of confirmations)
475
+ if (value.expires <= Date.now())
476
+ confirmations.delete(id);
477
+ if (confirmations.size >= 500)
478
+ confirmations.delete(confirmations.keys().next().value);
479
+ const token = randomUUID();
480
+ confirmations.set(sourceSessionId, { token, root: project.root, projectId: project.id, fingerprint: check.fingerprint, expires: Date.now() + 300_000 });
481
+ return { kind: 'empty', confirmationToken: token };
412
482
  }),
483
+ createWithInitialCommit: (sourceSessionId, confirmationToken) => outcome(() => withCreation(sourceSessionId, async (project) => {
484
+ const confirmation = confirmations.get(sourceSessionId);
485
+ confirmations.delete(sourceSessionId);
486
+ if (!confirmation || confirmation.token !== confirmationToken || confirmation.expires <= Date.now()
487
+ || confirmation.root !== project.root || confirmation.projectId !== project.id || !options.git.initializeEmptyRepository) {
488
+ throw domainError('stale_local', hostMessage('initialStateChanged'));
489
+ }
490
+ await options.git.initializeEmptyRepository(project.root, confirmation.fingerprint, async () => {
491
+ const current = await localProject(sourceSessionId);
492
+ if (current.id !== confirmation.projectId || current.root !== confirmation.root) {
493
+ throw domainError('stale_local', hostMessage('initialStateChanged'));
494
+ }
495
+ });
496
+ try {
497
+ return await createTarget(sourceSessionId);
498
+ }
499
+ catch {
500
+ throw domainError('git_operation_failed', hostMessage('initialCreatedWorktreeFailed'));
501
+ }
502
+ })),
503
+ create: sourceSessionId => outcome(() => withCreation(sourceSessionId, () => createTarget(sourceSessionId))),
413
504
  inspect: (sessionId, checkoutId) => outcome(async () => ({ target: await details(sessionId, checkoutId) })),
414
505
  reviewDiff: async (request) => {
415
506
  try {
416
507
  const record = await authorize(request.sessionId, request.checkoutId);
417
508
  if (record.ownerSessionId !== request.sessionId)
418
- return failure('not_owner', '只有 owner Isolated Session 可以读取验收 Diff');
509
+ return failure('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanReadTheReview'));
419
510
  if (record.revision !== request.expectedRevision)
420
- return failure('stale_target', 'Session Target 已变化,请刷新');
511
+ return failure('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
421
512
  const review = readyReview(record);
422
513
  if (review.reviewId !== request.expectedReviewId)
423
- return failure('stale_target', 'Review 身份已变化,请刷新');
514
+ return failure('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
424
515
  const before = await options.module.preflight?.(request.sessionId, request.expectedRevision);
425
516
  if (before === undefined || before.status === 'blocked')
426
517
  return preflightFailure(before);
427
518
  if (before.reviewId !== review.reviewId || before.isolatedHeadOid !== review.isolatedHeadOid) {
428
- return failure('stale_isolated', 'Ready 后 Isolated HEAD 已变化');
519
+ return failure('stale_isolated', hostMessage('isolatedHEADChangedAfterReady'));
429
520
  }
430
521
  const observed = await observe(record);
431
522
  const diff = await options.reviewDiff.read({
@@ -437,7 +528,7 @@ export function createWorktreeConsoleControlPlane(options) {
437
528
  });
438
529
  const after = await options.module.preflight?.(request.sessionId, request.expectedRevision);
439
530
  if (after === undefined || after.status === 'blocked') {
440
- return failure('stale_isolated', 'Ready 后 Isolated 内容已变化,Diff bytes 已丢弃');
531
+ return failure('stale_isolated', hostMessage('isolatedContentChangedAfterReadyDiffBytesWereDiscarded'));
441
532
  }
442
533
  const current = recordOf(options.registry, request.checkoutId);
443
534
  if (after.reviewId !== review.reviewId
@@ -446,7 +537,7 @@ export function createWorktreeConsoleControlPlane(options) {
446
537
  || current.delivery.state !== 'ready_for_review'
447
538
  || current.delivery.review.reviewId !== review.reviewId
448
539
  || current.delivery.review.isolatedFingerprint !== review.isolatedFingerprint)
449
- return failure('stale_isolated', 'Ready 后 Isolated 内容已变化,Diff bytes 已丢弃');
540
+ return failure('stale_isolated', hostMessage('isolatedContentChangedAfterReadyDiffBytesWereDiscarded'));
450
541
  return { ok: true, value: diff };
451
542
  }
452
543
  catch (error) {
@@ -458,31 +549,31 @@ export function createWorktreeConsoleControlPlane(options) {
458
549
  preflight: request => outcome(async () => {
459
550
  const record = await authorize(request.sessionId, request.checkoutId);
460
551
  if (record.ownerSessionId !== request.sessionId)
461
- throw domainError('not_owner', '只有 owner Isolated Session 可以执行同步预检');
552
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanRunSyncPreflight436'));
462
553
  if (record.revision !== request.expectedRevision)
463
- throw domainError('stale_target', 'Session Target 已变化,请刷新');
554
+ throw domainError('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
464
555
  const review = readyReview(record);
465
556
  if (review.reviewId !== request.expectedReviewId)
466
- throw domainError('stale_target', 'Review 身份已变化,请刷新');
557
+ throw domainError('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
467
558
  const preflight = await options.module.preflight?.(request.sessionId, request.expectedRevision);
468
559
  if (preflight === undefined)
469
- throw domainError('git_error', '当前 SessionCheckoutModule 不支持验收预检');
560
+ throw domainError('git_error', hostMessage('theCurrentSessionCheckoutModuleDoesNotSupportAcceptancePreflight'));
470
561
  return { preflight };
471
562
  }),
472
563
  previewRecoveryPreflight: request => outcome(async () => {
473
564
  const record = await authorize(request.sessionId, request.checkoutId);
474
565
  if (record.ownerSessionId !== request.sessionId)
475
- throw domainError('not_owner', '只有 owner Isolated Session 可以检查 Preview 恢复');
566
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanCheckPreviewRecovery440'));
476
567
  if (record.revision !== request.expectedRevision)
477
- throw domainError('stale_target', 'Session Target 已变化,请刷新');
568
+ throw domainError('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
478
569
  if (record.phase !== 'ready'
479
570
  || record.delivery.state !== 'preview_detached'
480
571
  || record.delivery.review.reviewId !== request.expectedReviewId
481
572
  || record.delivery.preview.previewId !== request.expectedPreviewId)
482
- throw domainError('stale_target', 'Detached Preview 身份已变化,请刷新');
573
+ throw domainError('stale_target', hostMessage('theDetachedPreviewIdentityHasChangedRefresh'));
483
574
  const preflight = await options.module.preflightPreviewRecovery?.(request.sessionId, request.expectedRevision, request.expectedReviewId, request.expectedPreviewId);
484
575
  if (preflight === undefined)
485
- throw domainError('git_error', '当前 SessionCheckoutModule 不支持 Preview Recovery 预检');
576
+ throw domainError('git_error', hostMessage('theCurrentSessionCheckoutModuleDoesNotSupportPreviewRecoveryPreflight'));
486
577
  return { preflight };
487
578
  }),
488
579
  preparePreviewRecoveryAnalysis: request => outcome(async () => {
@@ -493,7 +584,7 @@ export function createWorktreeConsoleControlPlane(options) {
493
584
  || request.recoveryProof.revision !== request.expectedRevision
494
585
  || request.recoveryProof.reviewId !== request.expectedReviewId
495
586
  || request.recoveryProof.previewId !== request.expectedPreviewId)
496
- throw domainError('stale_target', 'Preview Recovery 分析身份不匹配');
587
+ throw domainError('stale_target', hostMessage('thePreviewRecoveryAnalysisIdentityDoesNotMatch'));
497
588
  const prepared = await options.module.preparePreviewRecoveryAnalysis(request.sessionId, request.recoveryProof, `preview-recovery-analysis:${randomUUID()}`);
498
589
  const response = await mutationResponse(request.sessionId, request.checkoutId);
499
590
  return {
@@ -517,7 +608,7 @@ export function createWorktreeConsoleControlPlane(options) {
517
608
  || request.recoveryProof.revision !== request.expectedRevision
518
609
  || request.recoveryProof.reviewId !== request.expectedReviewId
519
610
  || request.recoveryProof.previewId !== request.expectedPreviewId)
520
- throw domainError('stale_target', 'Preview Recovery handoff 身份不匹配');
611
+ throw domainError('stale_target', hostMessage('thePreviewRecoveryHandoffIdentityDoesNotMatch'));
521
612
  const targetSessionId = createTargetSessionId();
522
613
  const launch = await options.module.createPreviewRecoveryHandoff(request.sessionId, request.recoveryProof, targetSessionId, `preview-recovery-handoff:${randomUUID()}`);
523
614
  const created = recordOf(options.registry, launch.target.checkout.id);
@@ -526,7 +617,7 @@ export function createWorktreeConsoleControlPlane(options) {
526
617
  || created.recoveryContinuation?.kind !== 'worktree_preview_recovery_handoff'
527
618
  || created.recoveryContinuation.requestId !== launch.continuation.requestId
528
619
  || observed.managedRoot !== launch.managedRoot)
529
- throw domainError('checkout_mismatch', 'Recovery handoff Worktree 的 Host 身份校验失败');
620
+ throw domainError('checkout_mismatch', hostMessage('hostIdentityVerificationFailedForTheRecoveryHandoffWorktree'));
530
621
  return {
531
622
  target: projectDetails(created, targetSessionId, observed.managedRoot, observed.snapshot, observed.dirty, false),
532
623
  targetSessionId,
@@ -546,10 +637,10 @@ export function createWorktreeConsoleControlPlane(options) {
546
637
  checkpoint: request => outcome(async () => {
547
638
  const record = await authorize(request.sessionId, request.checkoutId);
548
639
  if (record.ownerSessionId !== request.sessionId)
549
- throw domainError('not_owner', '只有 owner Isolated Session 可以保存阶段');
640
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanSaveACheckpoint447'));
550
641
  const commitMessage = request.commitMessage.trim();
551
642
  if (!commitMessage || commitMessage.length > 500 || !safeRecoveryRequestId(request.requestId)) {
552
- throw domainError('invalid_input', 'Checkpoint Commit Message 或 requestId 无效');
643
+ throw domainError('invalid_input', hostMessage('theCheckpointCommitMessageOrRequestIdIsInvalid'));
553
644
  }
554
645
  const result = await options.module.operate({
555
646
  action: 'checkpoint',
@@ -566,32 +657,32 @@ export function createWorktreeConsoleControlPlane(options) {
566
657
  return { ...(await mutationResponse(request.sessionId, request.checkoutId)), changedFiles: [...result.changedFiles] };
567
658
  }
568
659
  if (result.status !== 'checkpointed')
569
- throw domainError('operation_not_allowed', 'Checkpoint 返回了非预期状态');
660
+ throw domainError('operation_not_allowed', hostMessage('checkpointReturnedAnUnexpectedState'));
570
661
  const response = await mutationResponse(request.sessionId, request.checkoutId);
571
662
  if (response.target.state !== 'working' || response.target.revision !== result.target.revision) {
572
- throw domainError('checkout_mismatch', 'Checkpoint 后 Worktree 状态未收敛到 Working');
663
+ throw domainError('checkout_mismatch', hostMessage('worktreeDidNotReturnToWorkingAfterCheckpoint'));
573
664
  }
574
665
  return { ...response, checkpoint: result.checkpoint, changedFiles: [...result.changedFiles] };
575
666
  }),
576
667
  preview: request => outcome(async () => {
577
668
  const record = await authorize(request.sessionId, request.checkoutId);
578
669
  if (record.ownerSessionId !== request.sessionId)
579
- throw domainError('not_owner', '只有 owner Isolated Session 可以预览修改');
670
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanPreviewChanges'));
580
671
  if (record.revision !== request.expectedRevision)
581
- throw domainError('stale_target', 'Session Target 已变化,请刷新');
672
+ throw domainError('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
582
673
  const review = readyReview(record);
583
674
  if (review.reviewId !== request.expectedReviewId)
584
- throw domainError('stale_target', 'Review 身份已变化,请刷新');
675
+ throw domainError('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
585
676
  const result = await options.module.operate({
586
677
  action: 'preview', sessionId: request.sessionId, expectedRevision: request.expectedRevision,
587
678
  });
588
679
  if (result.status === 'error')
589
680
  throw domainError(result.code, result.message);
590
681
  if (result.status === 'conflict') {
591
- throw domainError('apply_conflict', 'Local Preview 预检发现内容冲突', applyConflictContinuation(result, review.reviewId));
682
+ throw domainError('apply_conflict', hostMessage('localPreviewPreflightFoundContentConflicts'), applyConflictContinuation(result, review.reviewId));
592
683
  }
593
684
  if (result.status !== 'previewed')
594
- throw domainError('operation_not_allowed', 'Preview 返回了非预期状态');
685
+ throw domainError('operation_not_allowed', hostMessage('previewReturnedAnUnexpectedState'));
595
686
  return {
596
687
  ...(await mutationResponse(request.sessionId, request.checkoutId)),
597
688
  changedFiles: [...result.changedFiles],
@@ -600,12 +691,12 @@ export function createWorktreeConsoleControlPlane(options) {
600
691
  resumeRevision: request => outcome(async () => {
601
692
  const record = await authorize(request.sessionId, request.checkoutId);
602
693
  if (record.ownerSessionId !== request.sessionId)
603
- throw domainError('not_owner', '只有 owner Isolated Session 可以继续修改');
694
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanResumeEditing456'));
604
695
  if (record.revision !== request.expectedRevision)
605
- throw domainError('stale_target', 'Session Target 已变化,请刷新');
696
+ throw domainError('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
606
697
  const review = readyReview(record);
607
698
  if (review.reviewId !== request.expectedReviewId)
608
- throw domainError('stale_target', 'Review 身份已变化,请刷新');
699
+ throw domainError('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
609
700
  const requestedRecovery = request.conflictContinuation;
610
701
  if (requestedRecovery && (requestedRecovery.kind !== 'worktree_apply_conflict'
611
702
  || !safeRecoveryRequestId(requestedRecovery.requestId)
@@ -615,7 +706,7 @@ export function createWorktreeConsoleControlPlane(options) {
615
706
  || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(requestedRecovery.localHeadOid)
616
707
  || requestedRecovery.conflictingFiles.length > 500
617
708
  || !requestedRecovery.conflictingFiles.every(safeConflictFile)))
618
- throw domainError('invalid_input', '冲突恢复请求身份无效');
709
+ throw domainError('invalid_input', hostMessage('invalidConflictRecoveryRequestIdentity'));
619
710
  const recovery = requestedRecovery ? {
620
711
  kind: 'worktree_apply_conflict',
621
712
  requestId: `conflict-recovery:${randomUUID()}`,
@@ -628,7 +719,7 @@ export function createWorktreeConsoleControlPlane(options) {
628
719
  ? await options.module.resumeRevision(request.sessionId, request.expectedRevision, request.expectedReviewId, recovery)
629
720
  : await options.module.resumeRevision(request.sessionId, request.expectedRevision, request.expectedReviewId);
630
721
  if (target.checkout.id !== request.checkoutId || target.delivery?.state !== 'working') {
631
- throw domainError('checkout_mismatch', '恢复编辑后 Worktree 身份或状态不一致');
722
+ throw domainError('checkout_mismatch', hostMessage('theWorktreeIdentityOrStateDoesNotMatchAfter'));
632
723
  }
633
724
  const response = await mutationResponse(request.sessionId, request.checkoutId);
634
725
  if (!requestedRecovery)
@@ -638,7 +729,7 @@ export function createWorktreeConsoleControlPlane(options) {
638
729
  || current.kind !== 'worktree_apply_conflict'
639
730
  || current.requestId !== recovery.requestId
640
731
  || current.workingRevision !== response.target.revision)
641
- throw domainError('checkout_mismatch', 'Host 未能持久化精确冲突恢复凭证');
732
+ throw domainError('checkout_mismatch', hostMessage('theHostCouldNotPersistTheExactConflictRecovery'));
642
733
  return {
643
734
  ...response,
644
735
  recoveryContinuation: {
@@ -655,12 +746,12 @@ export function createWorktreeConsoleControlPlane(options) {
655
746
  prepareReviewRegeneration: request => outcome(async () => {
656
747
  const record = await authorize(request.sessionId, request.checkoutId);
657
748
  if (record.ownerSessionId !== request.sessionId)
658
- throw domainError('not_owner', '只有 owner Isolated Session 可以请求重新生成验收结果');
749
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanRequestReviewRegeneration462'));
659
750
  if (record.revision !== request.expectedRevision)
660
- throw domainError('stale_target', 'Session Target 已变化,请刷新');
751
+ throw domainError('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
661
752
  const review = readyReview(record);
662
753
  if (review.reviewId !== request.expectedReviewId)
663
- throw domainError('stale_target', 'Review 身份已变化,请刷新');
754
+ throw domainError('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
664
755
  const prepared = await options.module.prepareReviewRegeneration(request.sessionId, request.expectedRevision, request.expectedReviewId, `review-regeneration:${randomUUID()}`);
665
756
  const response = await mutationResponse(request.sessionId, request.checkoutId);
666
757
  return {
@@ -677,7 +768,7 @@ export function createWorktreeConsoleControlPlane(options) {
677
768
  rollbackPreview: request => outcome(async () => {
678
769
  const record = await authorize(request.sessionId, request.checkoutId);
679
770
  if (record.ownerSessionId !== request.sessionId)
680
- throw domainError('not_owner', '只有 owner Isolated Session 可以撤回 Local Preview');
771
+ throw domainError('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanRollBackLocal'));
681
772
  const result = await options.module.operate({
682
773
  action: 'rollback_preview',
683
774
  sessionId: request.sessionId,
@@ -688,7 +779,7 @@ export function createWorktreeConsoleControlPlane(options) {
688
779
  if (result.status === 'error')
689
780
  throw domainError(result.code, result.message);
690
781
  if (result.status !== 'preview_rolled_back' && result.status !== 'preview_detached') {
691
- throw domainError('operation_not_allowed', 'Rollback Preview 返回了非预期状态');
782
+ throw domainError('operation_not_allowed', hostMessage('rollbackPreviewReturnedAnUnexpectedState'));
692
783
  }
693
784
  return {
694
785
  ...(await mutationResponse(request.sessionId, request.checkoutId)),
@@ -698,7 +789,7 @@ export function createWorktreeConsoleControlPlane(options) {
698
789
  discard: request => outcome(async () => {
699
790
  const record = await authorize(request.sessionId, request.checkoutId);
700
791
  if (record.ownerSessionId !== request.sessionId && ownerSessionAvailable(record)) {
701
- throw domainError('not_owner', 'Owner Session 已接管该 Worktree,只有 owner 可以 Discard');
792
+ throw domainError('not_owner', hostMessage('theOwnerSessionHasTakenOverThisWorktreeOnly467'));
702
793
  }
703
794
  if (record.ownerSessionId === request.sessionId) {
704
795
  const result = await options.module.operate({
@@ -714,7 +805,7 @@ export function createWorktreeConsoleControlPlane(options) {
714
805
  return { ...(await mutationResponse(request.sessionId, request.checkoutId)), changedFiles: [...result.changedFiles] };
715
806
  }
716
807
  if (result.status !== 'discarded')
717
- throw domainError('operation_not_allowed', 'Discard 返回了非预期状态');
808
+ throw domainError('operation_not_allowed', hostMessage('discardReturnedAnUnexpectedState'));
718
809
  }
719
810
  else {
720
811
  await options.module.manageManagedWorktreeForSession(request.sessionId, {
@@ -730,15 +821,15 @@ export function createWorktreeConsoleControlPlane(options) {
730
821
  try {
731
822
  const record = await authorize(request.sessionId, request.checkoutId);
732
823
  if (record.ownerSessionId !== request.sessionId)
733
- return failure('not_owner', '只有 owner Isolated Session 可以提交验收');
824
+ return failure('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanCommitAcceptance'));
734
825
  if (record.revision !== request.expectedRevision)
735
- return failure('stale_target', 'Session Target 已变化,请刷新');
826
+ return failure('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
736
827
  const review = readyReview(record);
737
828
  if (review.reviewId !== request.expectedReviewId)
738
- return failure('stale_target', 'Review 身份已变化,请刷新');
829
+ return failure('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
739
830
  const commitMessage = request.commitMessage.trim();
740
831
  if (!commitMessage || commitMessage.length > 500)
741
- return failure('invalid_input', 'Commit Message 必须为 1–500 个字符');
832
+ return failure('invalid_input', hostMessage('theCommitMessageMustContain1500Characters'));
742
833
  const result = await options.module.operate({
743
834
  sessionId: request.sessionId,
744
835
  expectedRevision: request.expectedRevision,
@@ -750,10 +841,10 @@ export function createWorktreeConsoleControlPlane(options) {
750
841
  if (result.status === 'error')
751
842
  return failure(result.code, result.message);
752
843
  if (result.status === 'conflict') {
753
- return failure('apply_conflict', 'Local 应用发生冲突', undefined, applyConflictContinuation(result, review.reviewId));
844
+ return failure('apply_conflict', hostMessage('applyingToLocalCausedConflicts'), undefined, applyConflictContinuation(result, review.reviewId));
754
845
  }
755
846
  if (result.status !== 'finished')
756
- return failure('operation_not_allowed', 'Finalize 返回了非预期状态');
847
+ return failure('operation_not_allowed', hostMessage('finalizeReturnedAnUnexpectedState'));
757
848
  return {
758
849
  ok: true,
759
850
  value: {
@@ -771,15 +862,15 @@ export function createWorktreeConsoleControlPlane(options) {
771
862
  try {
772
863
  const record = await authorize(request.sessionId, request.checkoutId);
773
864
  if (record.ownerSessionId !== request.sessionId)
774
- return failure('not_owner', '只有 owner Isolated Session 可以完成 Local Preview 验收');
865
+ return failure('not_owner', hostMessage('onlyTheOwnerIsolatedSessionCanFinalizeLocalPreview'));
775
866
  if (record.revision !== request.expectedRevision)
776
- return failure('stale_target', 'Session Target 已变化,请刷新');
867
+ return failure('stale_target', hostMessage('theSessionTargetHasChangedRefresh'));
777
868
  const review = previewReview(record);
778
869
  if (review.reviewId !== request.expectedReviewId)
779
- return failure('stale_target', 'Review 身份已变化,请刷新');
870
+ return failure('stale_target', hostMessage('theReviewIdentityHasChangedRefresh'));
780
871
  const commitMessage = request.commitMessage.trim();
781
872
  if (!commitMessage || commitMessage.length > 500)
782
- return failure('invalid_input', 'Commit Message 必须为 1–500 个字符');
873
+ return failure('invalid_input', hostMessage('theCommitMessageMustContain1500Characters'));
783
874
  const result = await options.module.operate({
784
875
  action: 'finalize_preview',
785
876
  sessionId: request.sessionId,
@@ -800,7 +891,7 @@ export function createWorktreeConsoleControlPlane(options) {
800
891
  };
801
892
  }
802
893
  if (result.status !== 'finished')
803
- return failure('operation_not_allowed', 'Finalize Preview 返回了非预期状态');
894
+ return failure('operation_not_allowed', hostMessage('finalizePreviewReturnedAnUnexpectedState'));
804
895
  return {
805
896
  ok: true,
806
897
  value: {
@@ -836,28 +927,28 @@ export function createWorktreeConsoleControlPlane(options) {
836
927
  beginNextIteration: request => outcome(async () => {
837
928
  const predecessor = recordOf(options.registry, request.checkoutId);
838
929
  if (predecessor.ownerSessionId !== request.sessionId) {
839
- throw domainError('not_owner', '只有 owner Session 可以开始下一轮');
930
+ throw domainError('not_owner', hostMessage('onlyTheOwnerSessionCanBeginTheNextIteration'));
840
931
  }
841
932
  if (predecessor.revision !== request.expectedRevision) {
842
- throw domainError('stale_target', 'Worktree 状态已变化,请刷新后再开始下一轮');
933
+ throw domainError('stale_target', hostMessage('theWorktreeStateHasChangedRefreshBeforeBeginningThe'));
843
934
  }
844
935
  if (predecessor.phase !== 'discarded' || predecessor.delivery.state !== 'delivered') {
845
- throw domainError('operation_not_allowed', '只有已成功清理的交付状态可以开始下一轮');
936
+ throw domainError('operation_not_allowed', hostMessage('onlyADeliveredStateWithSuccessfulCleanupCanBegin'));
846
937
  }
847
938
  const session = options.lookup.getSession(request.sessionId);
848
939
  const workspace = session?.projectId === undefined ? undefined : options.lookup.getProject(session.projectId);
849
940
  if (!workspace || !sameLocalRoot(resolve(workspace.root), resolve(predecessor.managedRoot))) {
850
- throw domainError('project_mismatch', '当前 Session 的 immutable cwd 与上一轮 Worktree 不一致');
941
+ throw domainError('project_mismatch', hostMessage('theCurrentSessionSImmutableCwdDoesNotMatch'));
851
942
  }
852
943
  const target = await options.module.beginNextIteration(request.sessionId, request.expectedRevision);
853
944
  if (target.checkout.kind !== 'isolated' || target.delivery?.state !== 'working') {
854
- throw domainError('checkout_mismatch', '下一轮 Worktree 未进入 working 状态');
945
+ throw domainError('checkout_mismatch', hostMessage('theNextWorktreeIterationDidNotEnterWorkingState484'));
855
946
  }
856
947
  const record = recordOf(options.registry, target.checkout.id);
857
948
  if (record.predecessorCheckoutId !== predecessor.checkoutId
858
949
  || record.ownerSessionId !== request.sessionId
859
950
  || !sameLocalRoot(record.managedRoot, predecessor.managedRoot)) {
860
- throw domainError('checkout_mismatch', '下一轮 Worktree 的 lineage 或 cwd 身份不一致');
951
+ throw domainError('checkout_mismatch', hostMessage('theNextIterationSWorktreeLineageOrCwdIdentity'));
861
952
  }
862
953
  const observed = await observe(record);
863
954
  return {