@cerbur/clutch-dsh-worktree 0.1.7 → 0.1.8

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 (102) hide show
  1. package/README.md +28 -8
  2. package/README.zh.md +21 -3
  3. package/lib/client/WorktreeSurface.d.ts.map +1 -1
  4. package/lib/client/WorktreeSurface.js +24 -8
  5. package/lib/client/WorktreeSurface.js.map +1 -1
  6. package/lib/client/locales.d.ts +12 -0
  7. package/lib/client/locales.d.ts.map +1 -1
  8. package/lib/client/locales.js +12 -0
  9. package/lib/client/locales.js.map +1 -1
  10. package/lib/client/worktree-context.js +1 -1
  11. package/lib/client/worktree-context.js.map +1 -1
  12. package/lib/client/worktree-error-copy.d.ts.map +1 -1
  13. package/lib/client/worktree-error-copy.js +8 -0
  14. package/lib/client/worktree-error-copy.js.map +1 -1
  15. package/lib/client/worktree-session.d.ts.map +1 -1
  16. package/lib/client/worktree-session.js +3 -1
  17. package/lib/client/worktree-session.js.map +1 -1
  18. package/lib/client/worktree-surface-rows.d.ts.map +1 -1
  19. package/lib/client/worktree-surface-rows.js +7 -0
  20. package/lib/client/worktree-surface-rows.js.map +1 -1
  21. package/lib/client.js +47 -11
  22. package/lib/client.js.map +1 -1
  23. package/lib/contract/index.d.ts +6 -2
  24. package/lib/contract/index.d.ts.map +1 -1
  25. package/lib/contract/index.js +4 -0
  26. package/lib/contract/index.js.map +1 -1
  27. package/lib/host/dsh-read-adapter.d.ts +1 -0
  28. package/lib/host/dsh-read-adapter.d.ts.map +1 -1
  29. package/lib/host/dsh-read-adapter.js +6 -0
  30. package/lib/host/dsh-read-adapter.js.map +1 -1
  31. package/lib/host/service.d.ts +2 -0
  32. package/lib/host/service.d.ts.map +1 -1
  33. package/lib/host/service.js +6 -1
  34. package/lib/host/service.js.map +1 -1
  35. package/lib/host/worktree-permission-manager.d.ts.map +1 -1
  36. package/lib/host/worktree-permission-manager.js +3 -1
  37. package/lib/host/worktree-permission-manager.js.map +1 -1
  38. package/lib/index.d.ts +2 -1
  39. package/lib/index.d.ts.map +1 -1
  40. package/lib/index.js +1 -0
  41. package/lib/index.js.map +1 -1
  42. package/lib/manage/manager-context.d.ts +2 -0
  43. package/lib/manage/manager-context.d.ts.map +1 -1
  44. package/lib/manage/manager-worktrees.d.ts +5 -0
  45. package/lib/manage/manager-worktrees.d.ts.map +1 -1
  46. package/lib/manage/manager-worktrees.js +125 -37
  47. package/lib/manage/manager-worktrees.js.map +1 -1
  48. package/lib/manage/manager.d.ts +20 -0
  49. package/lib/manage/manager.d.ts.map +1 -1
  50. package/lib/manage/manager.js +91 -13
  51. package/lib/manage/manager.js.map +1 -1
  52. package/lib/manage/types.d.ts +9 -1
  53. package/lib/manage/types.d.ts.map +1 -1
  54. package/lib/provider/git.d.ts +37 -9
  55. package/lib/provider/git.d.ts.map +1 -1
  56. package/lib/provider/git.js +162 -61
  57. package/lib/provider/git.js.map +1 -1
  58. package/lib/provider/index.d.ts +5 -1
  59. package/lib/provider/index.d.ts.map +1 -1
  60. package/lib/provider/index.js +4 -0
  61. package/lib/provider/index.js.map +1 -1
  62. package/lib/provider/mutation-lock.d.ts +37 -0
  63. package/lib/provider/mutation-lock.d.ts.map +1 -0
  64. package/lib/provider/mutation-lock.js +270 -0
  65. package/lib/provider/mutation-lock.js.map +1 -0
  66. package/lib/provider/mutation-token.d.ts +8 -0
  67. package/lib/provider/mutation-token.d.ts.map +1 -0
  68. package/lib/provider/mutation-token.js +20 -0
  69. package/lib/provider/mutation-token.js.map +1 -0
  70. package/lib/provider/repository-fingerprint.d.ts +8 -0
  71. package/lib/provider/repository-fingerprint.d.ts.map +1 -0
  72. package/lib/provider/repository-fingerprint.js +13 -0
  73. package/lib/provider/repository-fingerprint.js.map +1 -0
  74. package/lib/provider/sidecar-persistence.d.ts +7 -3
  75. package/lib/provider/sidecar-persistence.d.ts.map +1 -1
  76. package/lib/provider/sidecar-persistence.js +118 -44
  77. package/lib/provider/sidecar-persistence.js.map +1 -1
  78. package/lib/provider/sidecar-schema.d.ts +1 -1
  79. package/lib/provider/sidecar-schema.d.ts.map +1 -1
  80. package/lib/provider/sidecar-schema.js +140 -8
  81. package/lib/provider/sidecar-schema.js.map +1 -1
  82. package/lib/provider/sidecar.d.ts +3 -1
  83. package/lib/provider/sidecar.d.ts.map +1 -1
  84. package/lib/provider/sidecar.js +6 -1
  85. package/lib/provider/sidecar.js.map +1 -1
  86. package/lib/provider/subprocess.d.ts +37 -0
  87. package/lib/provider/subprocess.d.ts.map +1 -0
  88. package/lib/provider/subprocess.js +319 -0
  89. package/lib/provider/subprocess.js.map +1 -0
  90. package/lib/provider/transaction.d.ts +65 -0
  91. package/lib/provider/transaction.d.ts.map +1 -0
  92. package/lib/provider/transaction.js +1011 -0
  93. package/lib/provider/transaction.js.map +1 -0
  94. package/lib/provider/types.d.ts +98 -7
  95. package/lib/provider/types.d.ts.map +1 -1
  96. package/lib/provider/types.js +1 -1
  97. package/lib/provider/types.js.map +1 -1
  98. package/lib/typert.host.js +29 -25
  99. package/lib/typert.remote-client.d.ts +2 -2
  100. package/lib/typert.remote-client.d.ts.map +1 -1
  101. package/lib/typert.remote-client.js +29 -25
  102. package/package.json +3 -1
@@ -0,0 +1,1011 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { lstat, realpath, stat } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { CrossProcessMutationLock } from './mutation-lock.js';
5
+ import { createWorktreeMutationToken } from './mutation-token.js';
6
+ import { createRepositoryFingerprint } from './repository-fingerprint.js';
7
+ import { WorktreeProviderError, providerError } from './types.js';
8
+ function isMissing(error) {
9
+ return error.code === 'ENOENT';
10
+ }
11
+ async function pathExists(pathname) {
12
+ try {
13
+ await stat(pathname);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ if (isMissing(error))
18
+ return false;
19
+ throw error;
20
+ }
21
+ }
22
+ async function isDirectory(pathname) {
23
+ try {
24
+ return (await stat(pathname)).isDirectory();
25
+ }
26
+ catch (error) {
27
+ if (isMissing(error))
28
+ return false;
29
+ throw error;
30
+ }
31
+ }
32
+ async function canonicalPath(pathname) {
33
+ try {
34
+ return await realpath(pathname);
35
+ }
36
+ catch (error) {
37
+ if (isMissing(error))
38
+ return path.resolve(pathname);
39
+ throw error;
40
+ }
41
+ }
42
+ async function samePhysicalPath(left, right) {
43
+ if (path.resolve(left) === path.resolve(right))
44
+ return true;
45
+ try {
46
+ return (await realpath(left)) === (await realpath(right));
47
+ }
48
+ catch (error) {
49
+ if (isMissing(error))
50
+ return false;
51
+ throw error;
52
+ }
53
+ }
54
+ function sameRepository(left, right) {
55
+ return path.resolve(left.commonDirectory) === path.resolve(right.commonDirectory);
56
+ }
57
+ function isInside(parent, child) {
58
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
59
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
60
+ }
61
+ function removeRecoveryIssue(issues, operationId, worktreeId) {
62
+ return (issues ?? []).filter((issue) => issue.operationId !== operationId && (worktreeId === undefined || issue.worktreeId !== worktreeId));
63
+ }
64
+ function recoveryError(message, details = {}) {
65
+ return providerError('WORKTREE_RECOVERY_REQUIRED', message, details);
66
+ }
67
+ function normalizeGitError(operation, workspaceRoot, targetPath, error) {
68
+ if (error instanceof WorktreeProviderError)
69
+ return error;
70
+ return providerError('GIT_OPERATION_FAILED', `Git ${operation} failed: ${String(error)}`, {
71
+ operation,
72
+ workspaceRoot,
73
+ targetPath,
74
+ });
75
+ }
76
+ function recordForCreate(input) {
77
+ return {
78
+ worktreeId: input.worktreeId,
79
+ workspaceId: input.workspaceId,
80
+ absolutePath: input.targetPath,
81
+ branch: input.targetBranch,
82
+ source: 'plugin',
83
+ status: 'active',
84
+ };
85
+ }
86
+ function pendingCreate(input, repository) {
87
+ return {
88
+ id: randomUUID(),
89
+ type: 'create-worktree',
90
+ phase: 'executing',
91
+ workspaceId: input.workspaceId,
92
+ worktreeId: input.worktreeId,
93
+ targetPath: input.targetPath,
94
+ branch: input.targetBranch,
95
+ ...(input.newBranch !== undefined ? { baseRef: input.baseBranch } : {}),
96
+ repositoryFingerprint: createRepositoryFingerprint(repository),
97
+ startedAt: new Date().toISOString(),
98
+ };
99
+ }
100
+ function pendingRemove(input, record, repository) {
101
+ return {
102
+ id: randomUUID(),
103
+ type: 'remove-worktree',
104
+ phase: 'executing',
105
+ workspaceId: input.workspaceId,
106
+ worktreeId: input.worktreeId,
107
+ targetPath: record.absolutePath,
108
+ branch: record.branch,
109
+ source: record.source,
110
+ repositoryFingerprint: createRepositoryFingerprint(repository),
111
+ startedAt: new Date().toISOString(),
112
+ };
113
+ }
114
+ function activeBranchConflict(snapshot, branch, worktreeId) {
115
+ return snapshot.worktrees.find((record) => record.status === 'active' && record.branch === branch && record.worktreeId !== worktreeId);
116
+ }
117
+ export class WorktreeMutationTransaction {
118
+ dshHome;
119
+ git;
120
+ sidecar;
121
+ repositoryLock;
122
+ constructor(options) {
123
+ this.dshHome = path.resolve(options.dshHome);
124
+ this.git = options.git;
125
+ this.sidecar = options.sidecar;
126
+ this.repositoryLock = new CrossProcessMutationLock({
127
+ lockRoot: path.join(this.dshHome, 'clutch-dsh-worktree', 'locks'),
128
+ });
129
+ }
130
+ async create(input) {
131
+ return this.withShardLock(input.workspaceId, async (locked) => {
132
+ await this.git.validateRepository(input.workspaceRoot);
133
+ const repository = await this.resolveRepository(input.workspaceRoot);
134
+ return this.repositoryLock.run(`repository:${createRepositoryFingerprint(repository.identity)}`, async (lock) => {
135
+ lock.assertHeld();
136
+ const gitRoot = repository.identity.topLevel;
137
+ const current = await locked.read();
138
+ this.assertMutationAdmitted(current, input.workspaceId);
139
+ this.assertRepositoryCompatible(current, repository.identity, input.workspaceId);
140
+ this.assertGeneratedTarget(input);
141
+ if (this.git.listBranchesWithWorktreePaths) {
142
+ const branches = await this.git.listBranchesWithWorktreePaths(gitRoot);
143
+ const baseBranch = branches.find((branch) => branch.name === input.baseBranch);
144
+ if (!baseBranch) {
145
+ throw providerError('GIT_OPERATION_FAILED', `Local branch does not exist: ${input.baseBranch}`, {
146
+ workspaceRoot: input.workspaceRoot,
147
+ branch: input.baseBranch,
148
+ });
149
+ }
150
+ if (input.newBranch !== undefined && branches.some((branch) => branch.name === input.newBranch)) {
151
+ throw providerError('WORKTREE_BRANCH_CONFLICT', `New branch already exists: ${input.newBranch}`, {
152
+ workspaceRoot: input.workspaceRoot,
153
+ branch: input.newBranch,
154
+ baseBranch: input.baseBranch,
155
+ });
156
+ }
157
+ if (branches.some((branch) => branch.name === input.targetBranch && branch.worktreePath !== undefined)) {
158
+ throw providerError('WORKTREE_BRANCH_CONFLICT', `Branch is already checked out: ${input.targetBranch}`, {
159
+ workspaceRoot: input.workspaceRoot,
160
+ branch: input.targetBranch,
161
+ baseBranch: input.baseBranch,
162
+ });
163
+ }
164
+ }
165
+ else {
166
+ const branches = await this.git.listBranches(gitRoot);
167
+ if (!branches.includes(input.baseBranch)) {
168
+ throw providerError('GIT_OPERATION_FAILED', `Local branch does not exist: ${input.baseBranch}`, {
169
+ workspaceRoot: input.workspaceRoot,
170
+ branch: input.baseBranch,
171
+ });
172
+ }
173
+ if (input.newBranch !== undefined && branches.includes(input.newBranch)) {
174
+ throw providerError('WORKTREE_BRANCH_CONFLICT', `New branch already exists: ${input.newBranch}`, {
175
+ workspaceRoot: input.workspaceRoot,
176
+ branch: input.newBranch,
177
+ baseBranch: input.baseBranch,
178
+ });
179
+ }
180
+ const gitWorktrees = await this.git.listWorktrees(gitRoot);
181
+ if (gitWorktrees.some((worktree) => worktree.branch === input.targetBranch)) {
182
+ throw providerError('WORKTREE_BRANCH_CONFLICT', `Branch is already checked out: ${input.targetBranch}`, {
183
+ workspaceRoot: input.workspaceRoot,
184
+ branch: input.targetBranch,
185
+ baseBranch: input.baseBranch,
186
+ });
187
+ }
188
+ }
189
+ if (await pathExists(input.targetPath)) {
190
+ throw providerError('GIT_OPERATION_FAILED', `Generated Worktree path already exists: ${input.targetPath}`, {
191
+ workspaceRoot: input.workspaceRoot,
192
+ targetPath: input.targetPath,
193
+ worktreeId: input.worktreeId,
194
+ });
195
+ }
196
+ if (current.worktrees.some((record) => record.worktreeId === input.worktreeId)) {
197
+ throw providerError('SIDECAR_CORRUPT', `Generated Worktree ID is already recorded: ${input.worktreeId}`, {
198
+ worktreeId: input.worktreeId,
199
+ });
200
+ }
201
+ const sidecarConflict = activeBranchConflict(current, input.targetBranch);
202
+ if (sidecarConflict) {
203
+ throw providerError('WORKTREE_BRANCH_CONFLICT', `Branch is already recorded as active: ${input.targetBranch}`, {
204
+ branch: input.targetBranch,
205
+ worktreeId: sidecarConflict.worktreeId,
206
+ });
207
+ }
208
+ const record = recordForCreate(input);
209
+ const pending = pendingCreate(input, repository.identity);
210
+ await locked.mutate((snapshot) => {
211
+ const { repository: _repository, ...withoutRepository } = snapshot;
212
+ void _repository;
213
+ return {
214
+ result: undefined,
215
+ snapshot: {
216
+ ...withoutRepository,
217
+ repositoryFingerprint: createRepositoryFingerprint(repository.identity),
218
+ pendingOperation: pending,
219
+ },
220
+ };
221
+ });
222
+ try {
223
+ await this.git.createWorktree(input.workspaceRoot, input.targetPath, input.baseBranch, input.newBranch);
224
+ }
225
+ catch (error) {
226
+ return this.reconcileCreateFailure({
227
+ locked,
228
+ input,
229
+ record,
230
+ pending,
231
+ repository: repository.identity,
232
+ gitRoot,
233
+ error,
234
+ });
235
+ }
236
+ lock.assertHeld();
237
+ let live;
238
+ try {
239
+ live = await this.git.listWorktrees(gitRoot);
240
+ }
241
+ catch (inspectionError) {
242
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
243
+ throw recoveryError(`Unable to verify Git after create: ${input.targetPath}`, {
244
+ workspaceId: input.workspaceId,
245
+ operationId: pending.id,
246
+ targetPath: input.targetPath,
247
+ cause: String(inspectionError),
248
+ });
249
+ }
250
+ if (!(await this.isExactCreatedWorktree(live, input))) {
251
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
252
+ throw recoveryError(`Git create completed without the expected Worktree: ${input.targetPath}`, {
253
+ workspaceId: input.workspaceId,
254
+ worktreeId: input.worktreeId,
255
+ targetPath: input.targetPath,
256
+ });
257
+ }
258
+ await this.publishCreated(locked, pending.id, record, repository.identity);
259
+ return record;
260
+ });
261
+ });
262
+ }
263
+ async remove(input) {
264
+ return this.withShardLock(input.workspaceId, async (locked) => {
265
+ await this.git.validateRepository(input.workspaceRoot);
266
+ const repository = await this.resolveRepository(input.workspaceRoot);
267
+ return this.repositoryLock.run(`repository:${createRepositoryFingerprint(repository.identity)}`, async (lock) => {
268
+ lock.assertHeld();
269
+ const gitRoot = repository.identity.topLevel;
270
+ const current = await locked.read();
271
+ this.assertMutationAdmitted(current, input.workspaceId);
272
+ this.assertRepositoryCompatible(current, repository.identity, input.workspaceId);
273
+ const record = current.worktrees.find((candidate) => candidate.worktreeId === input.worktreeId);
274
+ if (!record) {
275
+ throw providerError('WORKTREE_NOT_FOUND', `Worktree not found: ${input.worktreeId}`, {
276
+ workspaceId: input.workspaceId,
277
+ worktreeId: input.worktreeId,
278
+ });
279
+ }
280
+ if (record.status === 'removed') {
281
+ throw providerError('WORKTREE_REMOVED', `Worktree has already been removed: ${input.worktreeId}`, {
282
+ workspaceId: input.workspaceId,
283
+ worktreeId: input.worktreeId,
284
+ });
285
+ }
286
+ this.assertMutationToken(current, record, input.mutationToken);
287
+ if (await samePhysicalPath(record.absolutePath, gitRoot)) {
288
+ throw providerError('WORKTREE_STATE_CONFLICT', 'The main Worktree cannot be removed', {
289
+ workspaceId: input.workspaceId,
290
+ worktreeId: input.worktreeId,
291
+ targetPath: record.absolutePath,
292
+ });
293
+ }
294
+ const liveBefore = await this.git.listWorktrees(gitRoot);
295
+ const exactBefore = await this.findExactWorktree(liveBefore, record.absolutePath, record.branch);
296
+ if (!exactBefore) {
297
+ const pathRegistration = await this.findWorktreeByPhysicalPath(liveBefore, record.absolutePath);
298
+ const branchRegistration = liveBefore.find((worktree) => worktree.branch === record.branch);
299
+ if (!pathRegistration && !branchRegistration && !(await pathExists(record.absolutePath))) {
300
+ // A legacy caller may have completed Git removal before the v3
301
+ // journal was introduced. With no path or matching registration
302
+ // left to delete, finalizing the relation is safe and idempotent.
303
+ const pending = pendingRemove(input, record, repository.identity);
304
+ await locked.mutate((snapshot) => {
305
+ const { repository: _repository, ...withoutRepository } = snapshot;
306
+ void _repository;
307
+ return {
308
+ result: undefined,
309
+ snapshot: {
310
+ ...withoutRepository,
311
+ repositoryFingerprint: createRepositoryFingerprint(repository.identity),
312
+ pendingOperation: pending,
313
+ },
314
+ };
315
+ });
316
+ await this.publishRemoved(locked, pending.id, input.worktreeId, repository.identity);
317
+ return;
318
+ }
319
+ await this.markRecovery(locked, {
320
+ id: randomUUID(),
321
+ type: 'remove-worktree',
322
+ phase: 'recovery-needed',
323
+ workspaceId: input.workspaceId,
324
+ worktreeId: input.worktreeId,
325
+ targetPath: record.absolutePath,
326
+ branch: record.branch,
327
+ source: record.source,
328
+ repositoryFingerprint: createRepositoryFingerprint(repository.identity),
329
+ startedAt: new Date().toISOString(),
330
+ }, 'WORKTREE_IDENTITY_CHANGED');
331
+ throw providerError('WORKTREE_IDENTITY_CHANGED', `Worktree identity changed: ${record.absolutePath}`, {
332
+ workspaceId: input.workspaceId,
333
+ worktreeId: input.worktreeId,
334
+ targetPath: record.absolutePath,
335
+ });
336
+ }
337
+ await this.assertSafeRemovalPath(record, input.workspaceRoot);
338
+ const pending = pendingRemove(input, record, repository.identity);
339
+ await locked.mutate((snapshot) => {
340
+ const { repository: _repository, ...withoutRepository } = snapshot;
341
+ void _repository;
342
+ return {
343
+ result: undefined,
344
+ snapshot: {
345
+ ...withoutRepository,
346
+ repositoryFingerprint: createRepositoryFingerprint(repository.identity),
347
+ pendingOperation: pending,
348
+ },
349
+ };
350
+ });
351
+ try {
352
+ await this.git.removeWorktree(input.workspaceRoot, record.absolutePath);
353
+ }
354
+ catch (error) {
355
+ return this.reconcileRemoveFailure({
356
+ locked,
357
+ input,
358
+ record,
359
+ pending,
360
+ repository: repository.identity,
361
+ gitRoot,
362
+ error,
363
+ });
364
+ }
365
+ lock.assertHeld();
366
+ let liveAfter;
367
+ try {
368
+ liveAfter = await this.git.listWorktrees(gitRoot);
369
+ }
370
+ catch (inspectionError) {
371
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
372
+ throw recoveryError(`Unable to verify Git after remove: ${record.absolutePath}`, {
373
+ workspaceId: input.workspaceId,
374
+ operationId: pending.id,
375
+ targetPath: record.absolutePath,
376
+ cause: String(inspectionError),
377
+ });
378
+ }
379
+ if (await this.findExactWorktree(liveAfter, record.absolutePath, record.branch)) {
380
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
381
+ throw recoveryError(`Git remove did not remove the registered Worktree: ${record.absolutePath}`, {
382
+ workspaceId: input.workspaceId,
383
+ worktreeId: input.worktreeId,
384
+ targetPath: record.absolutePath,
385
+ });
386
+ }
387
+ let targetExists;
388
+ try {
389
+ targetExists = await pathExists(record.absolutePath);
390
+ }
391
+ catch (inspectionError) {
392
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
393
+ throw recoveryError(`Unable to verify the removed Worktree path: ${record.absolutePath}`, {
394
+ workspaceId: input.workspaceId,
395
+ operationId: pending.id,
396
+ targetPath: record.absolutePath,
397
+ cause: String(inspectionError),
398
+ });
399
+ }
400
+ if (targetExists) {
401
+ await this.markRecovery(locked, pending, 'WORKTREE_IDENTITY_CHANGED');
402
+ throw providerError('WORKTREE_IDENTITY_CHANGED', `Worktree path remains after Git removal: ${record.absolutePath}`, {
403
+ workspaceId: input.workspaceId,
404
+ worktreeId: input.worktreeId,
405
+ targetPath: record.absolutePath,
406
+ });
407
+ }
408
+ await this.publishRemoved(locked, pending.id, input.worktreeId, repository.identity);
409
+ });
410
+ });
411
+ }
412
+ async import(input) {
413
+ if (!path.isAbsolute(input.absolutePath)) {
414
+ throw providerError('WORKTREE_IMPORT_INVALID', 'An absolute Worktree path is required', {
415
+ workspaceId: input.workspaceId,
416
+ absolutePath: input.absolutePath,
417
+ });
418
+ }
419
+ const requestedPath = path.resolve(input.absolutePath);
420
+ if (!(await isDirectory(requestedPath))) {
421
+ throw providerError('WORKTREE_IMPORT_INVALID', `Worktree path is not a directory: ${requestedPath}`, {
422
+ workspaceId: input.workspaceId,
423
+ absolutePath: requestedPath,
424
+ });
425
+ }
426
+ return this.withShardLock(input.workspaceId, async (locked) => {
427
+ await this.git.validateRepository(input.workspaceRoot);
428
+ const repository = await this.resolveRepository(input.workspaceRoot);
429
+ return this.repositoryLock.run(`repository:${createRepositoryFingerprint(repository.identity)}`, async (lock) => {
430
+ lock.assertHeld();
431
+ const gitRoot = repository.identity.topLevel;
432
+ const current = await locked.read();
433
+ this.assertMutationAdmitted(current, input.workspaceId);
434
+ this.assertRepositoryCompatible(current, repository.identity, input.workspaceId);
435
+ return locked.mutate(async (snapshot) => {
436
+ const repositoryFingerprint = createRepositoryFingerprint(repository.identity);
437
+ const liveWorktree = await this.findWorktreeByPhysicalPath(await this.git.listWorktrees(gitRoot), requestedPath);
438
+ if (!liveWorktree ||
439
+ !liveWorktree.branch ||
440
+ await samePhysicalPath(liveWorktree.absolutePath, gitRoot)) {
441
+ throw providerError('WORKTREE_IMPORT_INVALID', `Path is not an importable Worktree: ${requestedPath}`, {
442
+ workspaceId: input.workspaceId,
443
+ absolutePath: requestedPath,
444
+ });
445
+ }
446
+ const normalizedPath = await canonicalPath(liveWorktree.absolutePath);
447
+ const existing = await this.findSidecarWorktreeByPhysicalPath(snapshot.worktrees, normalizedPath);
448
+ if (existing) {
449
+ if (existing.status === 'active' && existing.source === 'external' && existing.workspaceId === input.workspaceId) {
450
+ return {
451
+ result: existing,
452
+ snapshot: snapshot.repositoryFingerprint === repositoryFingerprint
453
+ ? snapshot
454
+ : { ...snapshot, repositoryFingerprint },
455
+ changed: snapshot.repositoryFingerprint === repositoryFingerprint ? false : undefined,
456
+ };
457
+ }
458
+ throw providerError('WORKTREE_ALREADY_MANAGED', `Worktree path is already managed: ${normalizedPath}`, {
459
+ workspaceId: input.workspaceId,
460
+ absolutePath: normalizedPath,
461
+ worktreeId: existing.worktreeId,
462
+ source: existing.source,
463
+ status: existing.status,
464
+ });
465
+ }
466
+ if (snapshot.worktrees.some((candidate) => candidate.worktreeId === input.worktreeId)) {
467
+ throw providerError('SIDECAR_CORRUPT', `Generated Worktree ID is already recorded: ${input.worktreeId}`, {
468
+ worktreeId: input.worktreeId,
469
+ });
470
+ }
471
+ const record = {
472
+ worktreeId: input.worktreeId,
473
+ workspaceId: input.workspaceId,
474
+ absolutePath: normalizedPath,
475
+ branch: liveWorktree.branch,
476
+ source: 'external',
477
+ status: 'active',
478
+ };
479
+ return {
480
+ result: record,
481
+ snapshot: {
482
+ ...snapshot,
483
+ repositoryFingerprint,
484
+ worktrees: [...snapshot.worktrees, record],
485
+ },
486
+ };
487
+ });
488
+ });
489
+ });
490
+ }
491
+ async recover(input) {
492
+ return this.withShardLock(input.workspaceId, async (locked) => {
493
+ await this.git.validateRepository(input.workspaceRoot);
494
+ const repository = await this.resolveRepository(input.workspaceRoot);
495
+ return this.repositoryLock.run(`repository:${createRepositoryFingerprint(repository.identity)}`, async (lock) => {
496
+ lock.assertHeld();
497
+ const gitRoot = repository.identity.topLevel;
498
+ const current = await locked.read();
499
+ this.assertRepositoryCompatible(current, repository.identity, input.workspaceId, true);
500
+ const pending = current.pendingOperation;
501
+ let live;
502
+ try {
503
+ live = await this.git.listWorktrees(gitRoot);
504
+ }
505
+ catch (inspectionError) {
506
+ if (pending)
507
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
508
+ throw recoveryError(`Unable to inspect Git during Worktree recovery`, {
509
+ workspaceId: input.workspaceId,
510
+ ...(pending ? { operationId: pending.id } : {}),
511
+ cause: String(inspectionError),
512
+ });
513
+ }
514
+ if (!pending) {
515
+ const stale = await this.findStaleActiveRecord(current, live);
516
+ if (stale) {
517
+ await this.appendRecoveryIssue(locked, {
518
+ code: 'WORKTREE_RECOVERY_REQUIRED',
519
+ worktreeId: stale.worktreeId,
520
+ observedAt: new Date().toISOString(),
521
+ });
522
+ throw recoveryError(`Active Worktree is not registered in Git: ${stale.absolutePath}`, {
523
+ workspaceId: input.workspaceId,
524
+ worktreeId: stale.worktreeId,
525
+ targetPath: stale.absolutePath,
526
+ });
527
+ }
528
+ return;
529
+ }
530
+ const pendingFingerprint = pending.repositoryFingerprint ??
531
+ (pending.repository ? createRepositoryFingerprint(pending.repository) : undefined);
532
+ if (pendingFingerprint !== createRepositoryFingerprint(repository.identity)) {
533
+ await this.markRecovery(locked, pending, 'WORKTREE_IDENTITY_CHANGED');
534
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Pending operation belongs to another repository', {
535
+ workspaceId: input.workspaceId,
536
+ operationId: pending.id,
537
+ });
538
+ }
539
+ if (pending.type === 'create-worktree') {
540
+ try {
541
+ await this.assertRecoverableCreatePath(pending, input.workspaceRoot);
542
+ }
543
+ catch (error) {
544
+ await this.markRecovery(locked, pending, 'WORKTREE_IDENTITY_CHANGED');
545
+ if (error instanceof WorktreeProviderError)
546
+ throw error;
547
+ throw recoveryError(`Unable to validate the pending Worktree path: ${pending.targetPath}`, {
548
+ workspaceId: input.workspaceId,
549
+ operationId: pending.id,
550
+ targetPath: pending.targetPath,
551
+ cause: String(error),
552
+ });
553
+ }
554
+ const exact = await this.isExactCreatedWorktree(live, {
555
+ workspaceId: input.workspaceId,
556
+ workspaceRoot: input.workspaceRoot,
557
+ targetPath: pending.targetPath,
558
+ worktreeId: pending.worktreeId,
559
+ baseBranch: pending.baseRef ?? pending.branch,
560
+ newBranch: pending.baseRef !== undefined ? pending.branch : undefined,
561
+ targetBranch: pending.branch,
562
+ });
563
+ if (exact) {
564
+ const record = {
565
+ worktreeId: pending.worktreeId,
566
+ workspaceId: input.workspaceId,
567
+ absolutePath: pending.targetPath,
568
+ branch: pending.branch,
569
+ source: 'plugin',
570
+ status: 'active',
571
+ };
572
+ await this.publishCreated(locked, pending.id, record, repository.identity);
573
+ return;
574
+ }
575
+ let branches;
576
+ try {
577
+ branches = await this.git.listBranches(gitRoot);
578
+ }
579
+ catch (inspectionError) {
580
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
581
+ throw recoveryError(`Unable to inspect branches during pending create recovery`, {
582
+ workspaceId: input.workspaceId,
583
+ operationId: pending.id,
584
+ targetPath: pending.targetPath,
585
+ cause: String(inspectionError),
586
+ });
587
+ }
588
+ const ambiguousNewBranch = pending.baseRef !== undefined && branches.includes(pending.branch);
589
+ let pendingTargetExists;
590
+ try {
591
+ pendingTargetExists = await pathExists(pending.targetPath);
592
+ }
593
+ catch (inspectionError) {
594
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
595
+ throw recoveryError(`Unable to inspect the pending Worktree path: ${pending.targetPath}`, {
596
+ workspaceId: input.workspaceId,
597
+ operationId: pending.id,
598
+ targetPath: pending.targetPath,
599
+ cause: String(inspectionError),
600
+ });
601
+ }
602
+ if (!ambiguousNewBranch && !pendingTargetExists) {
603
+ await this.clearPending(locked, pending.id);
604
+ return;
605
+ }
606
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
607
+ throw recoveryError(`Unable to reconcile pending create: ${pending.targetPath}`, {
608
+ workspaceId: input.workspaceId,
609
+ operationId: pending.id,
610
+ targetPath: pending.targetPath,
611
+ });
612
+ }
613
+ const exact = await this.findExactWorktree(live, pending.targetPath, pending.branch);
614
+ let pendingTargetExists;
615
+ try {
616
+ pendingTargetExists = await pathExists(pending.targetPath);
617
+ }
618
+ catch (inspectionError) {
619
+ await this.markRecovery(locked, pending, 'WORKTREE_RECOVERY_REQUIRED');
620
+ throw recoveryError(`Unable to inspect the pending Worktree path: ${pending.targetPath}`, {
621
+ workspaceId: input.workspaceId,
622
+ operationId: pending.id,
623
+ targetPath: pending.targetPath,
624
+ cause: String(inspectionError),
625
+ });
626
+ }
627
+ if (!exact && !pendingTargetExists) {
628
+ await this.publishRemoved(locked, pending.id, pending.worktreeId, repository.identity);
629
+ return;
630
+ }
631
+ if (exact) {
632
+ await this.clearPending(locked, pending.id);
633
+ return;
634
+ }
635
+ await this.markRecovery(locked, pending, 'WORKTREE_IDENTITY_CHANGED');
636
+ throw providerError('WORKTREE_IDENTITY_CHANGED', `Unable to reconcile pending remove: ${pending.targetPath}`, {
637
+ workspaceId: input.workspaceId,
638
+ operationId: pending.id,
639
+ targetPath: pending.targetPath,
640
+ });
641
+ });
642
+ });
643
+ }
644
+ async withShardLock(workspaceId, operation) {
645
+ if (this.sidecar.runExclusive)
646
+ return this.sidecar.runExclusive(workspaceId, operation);
647
+ // Injected legacy stores remain usable for tests and older compositions, but
648
+ // cannot provide the cross-process shard lock until they implement the seam.
649
+ return operation({
650
+ read: () => this.sidecar.read(workspaceId),
651
+ mutate: (mutation) => this.sidecar.mutate(workspaceId, mutation),
652
+ });
653
+ }
654
+ async resolveRepository(workspaceRoot) {
655
+ if (this.git.resolveRepositoryIdentity)
656
+ return this.git.resolveRepositoryIdentity(workspaceRoot);
657
+ const topLevel = this.git.resolveRepositoryRoot
658
+ ? await this.git.resolveRepositoryRoot(workspaceRoot)
659
+ : workspaceRoot;
660
+ const canonicalTopLevel = await canonicalPath(topLevel);
661
+ const commonDirectory = await canonicalPath(path.join(canonicalTopLevel, '.git'));
662
+ return { identity: { topLevel: canonicalTopLevel, commonDirectory } };
663
+ }
664
+ assertMutationAdmitted(snapshot, workspaceId) {
665
+ if (snapshot.pendingOperation) {
666
+ throw recoveryError(`Workspace has a pending Worktree operation: ${snapshot.pendingOperation.id}`, {
667
+ workspaceId,
668
+ operationId: snapshot.pendingOperation.id,
669
+ });
670
+ }
671
+ if (snapshot.recoveryIssues && snapshot.recoveryIssues.length > 0) {
672
+ throw recoveryError(`Workspace has unresolved Worktree recovery issues: ${workspaceId}`, { workspaceId });
673
+ }
674
+ }
675
+ assertMutationToken(snapshot, record, token) {
676
+ const expected = createWorktreeMutationToken(snapshot, record);
677
+ if (token === undefined || token !== expected) {
678
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Worktree state changed after it was loaded', {
679
+ workspaceId: record.workspaceId,
680
+ worktreeId: record.worktreeId,
681
+ });
682
+ }
683
+ }
684
+ assertRepositoryCompatible(snapshot, identity, workspaceId, allowMissing = false) {
685
+ const actualFingerprint = createRepositoryFingerprint(identity);
686
+ if (snapshot.repositoryFingerprint && snapshot.repositoryFingerprint !== actualFingerprint) {
687
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Workspace repository identity changed', {
688
+ workspaceId,
689
+ expectedFingerprint: snapshot.repositoryFingerprint,
690
+ actualFingerprint,
691
+ });
692
+ }
693
+ if (snapshot.repository && !sameRepository(snapshot.repository, identity)) {
694
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Workspace repository identity changed', {
695
+ workspaceId,
696
+ expectedCommonDirectory: snapshot.repository.commonDirectory,
697
+ actualCommonDirectory: identity.commonDirectory,
698
+ });
699
+ }
700
+ if (!allowMissing && !identity.commonDirectory) {
701
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Unable to resolve Workspace repository identity', { workspaceId });
702
+ }
703
+ }
704
+ assertGeneratedTarget(input) {
705
+ const generatedRoot = path.join(this.dshHome, 'clutch-dsh-worktree', 'worktree');
706
+ if (!path.isAbsolute(input.targetPath) ||
707
+ !isInside(generatedRoot, input.targetPath) ||
708
+ isInside(input.workspaceRoot, input.targetPath)) {
709
+ throw providerError('WORKTREE_STATE_CONFLICT', 'Generated Worktree path is outside the managed boundary', {
710
+ workspaceId: input.workspaceId,
711
+ targetPath: input.targetPath,
712
+ });
713
+ }
714
+ }
715
+ async assertSafeRemovalPath(record, workspaceRoot) {
716
+ const targetPath = path.resolve(record.absolutePath);
717
+ if (record.source === 'plugin') {
718
+ const generatedRoot = path.join(this.dshHome, 'clutch-dsh-worktree', 'worktree');
719
+ if (!isInside(generatedRoot, targetPath) || isInside(workspaceRoot, targetPath)) {
720
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Managed Worktree path moved outside its trusted root', {
721
+ worktreeId: record.worktreeId,
722
+ targetPath: record.absolutePath,
723
+ });
724
+ }
725
+ for (const trustedPath of [
726
+ this.dshHome,
727
+ path.join(this.dshHome, 'clutch-dsh-worktree'),
728
+ generatedRoot,
729
+ targetPath,
730
+ ]) {
731
+ await this.assertNotSymlink(trustedPath, record);
732
+ }
733
+ return;
734
+ }
735
+ // Imported records are canonicalized at registration time. Refusing a
736
+ // symlink here prevents a later path replacement from redirecting Git's
737
+ // destructive command to an unrelated directory.
738
+ await this.assertNotSymlink(targetPath, record);
739
+ }
740
+ async assertRecoverableCreatePath(pending, workspaceRoot) {
741
+ const targetPath = path.resolve(pending.targetPath);
742
+ const generatedRoot = path.join(this.dshHome, 'clutch-dsh-worktree', 'worktree');
743
+ if (!isInside(generatedRoot, targetPath) || isInside(workspaceRoot, targetPath)) {
744
+ throw providerError('WORKTREE_IDENTITY_CHANGED', 'Pending Worktree path moved outside its trusted root', {
745
+ worktreeId: pending.worktreeId,
746
+ targetPath: pending.targetPath,
747
+ });
748
+ }
749
+ const record = {
750
+ worktreeId: pending.worktreeId,
751
+ workspaceId: pending.workspaceId,
752
+ absolutePath: pending.targetPath,
753
+ branch: pending.branch,
754
+ source: 'plugin',
755
+ status: 'active',
756
+ };
757
+ for (const trustedPath of [
758
+ this.dshHome,
759
+ path.join(this.dshHome, 'clutch-dsh-worktree'),
760
+ generatedRoot,
761
+ targetPath,
762
+ ]) {
763
+ await this.assertNotSymlink(trustedPath, record);
764
+ }
765
+ }
766
+ async assertNotSymlink(pathname, record) {
767
+ try {
768
+ if ((await lstat(pathname)).isSymbolicLink()) {
769
+ throw providerError('WORKTREE_IDENTITY_CHANGED', `Worktree path is a symlink: ${pathname}`, {
770
+ worktreeId: record.worktreeId,
771
+ targetPath: record.absolutePath,
772
+ });
773
+ }
774
+ }
775
+ catch (error) {
776
+ if (isMissing(error))
777
+ return;
778
+ throw error;
779
+ }
780
+ }
781
+ async findExactWorktree(worktrees, targetPath, branch) {
782
+ for (const worktree of worktrees) {
783
+ if (worktree.branch === branch && await samePhysicalPath(worktree.absolutePath, targetPath))
784
+ return worktree;
785
+ }
786
+ return undefined;
787
+ }
788
+ async findWorktreeByPhysicalPath(worktrees, targetPath) {
789
+ for (const worktree of worktrees) {
790
+ if (await samePhysicalPath(worktree.absolutePath, targetPath))
791
+ return worktree;
792
+ }
793
+ return undefined;
794
+ }
795
+ async findSidecarWorktreeByPhysicalPath(worktrees, targetPath) {
796
+ for (const worktree of worktrees) {
797
+ if (await samePhysicalPath(worktree.absolutePath, targetPath))
798
+ return worktree;
799
+ }
800
+ return undefined;
801
+ }
802
+ async isExactCreatedWorktree(worktrees, input) {
803
+ const exact = await this.findExactWorktree(worktrees, input.targetPath, input.targetBranch);
804
+ return exact !== undefined && exact.detached !== true;
805
+ }
806
+ async findStaleActiveRecord(snapshot, worktrees) {
807
+ for (const record of snapshot.worktrees) {
808
+ if (record.status !== 'active')
809
+ continue;
810
+ if (!(await this.findExactWorktree(worktrees, record.absolutePath, record.branch)))
811
+ return record;
812
+ }
813
+ return undefined;
814
+ }
815
+ async publishCreated(locked, operationId, record, repository) {
816
+ await locked.mutate((snapshot) => {
817
+ const existing = snapshot.worktrees.find((candidate) => candidate.worktreeId === record.worktreeId);
818
+ if (existing && JSON.stringify(existing) !== JSON.stringify(record)) {
819
+ throw recoveryError(`Pending create conflicts with existing Worktree record: ${record.worktreeId}`, {
820
+ worktreeId: record.worktreeId,
821
+ operationId,
822
+ });
823
+ }
824
+ const conflict = activeBranchConflict(snapshot, record.branch, record.worktreeId);
825
+ if (conflict) {
826
+ throw recoveryError(`Pending create conflicts with active branch: ${record.branch}`, {
827
+ worktreeId: record.worktreeId,
828
+ operationId,
829
+ });
830
+ }
831
+ const recoveryIssues = removeRecoveryIssue(snapshot.recoveryIssues, operationId, record.worktreeId);
832
+ const { repository: _repository, ...withoutRepository } = snapshot;
833
+ void _repository;
834
+ return {
835
+ result: undefined,
836
+ snapshot: {
837
+ ...withoutRepository,
838
+ repositoryFingerprint: createRepositoryFingerprint(repository),
839
+ worktrees: existing ? snapshot.worktrees : [...snapshot.worktrees, record],
840
+ pendingOperation: undefined,
841
+ ...(recoveryIssues.length > 0 ? { recoveryIssues } : {}),
842
+ },
843
+ };
844
+ });
845
+ }
846
+ async publishRemoved(locked, operationId, worktreeId, repository) {
847
+ await locked.mutate((snapshot) => {
848
+ const recoveryIssues = removeRecoveryIssue(snapshot.recoveryIssues, operationId, worktreeId);
849
+ const { repository: _repository, ...withoutRepository } = snapshot;
850
+ void _repository;
851
+ return {
852
+ result: undefined,
853
+ snapshot: {
854
+ ...withoutRepository,
855
+ repositoryFingerprint: createRepositoryFingerprint(repository),
856
+ worktrees: snapshot.worktrees.map((record) => record.worktreeId === worktreeId ? { ...record, status: 'removed' } : record),
857
+ bindings: snapshot.bindings.map((binding) => binding.worktreeId === worktreeId && binding.status === 'active'
858
+ ? { ...binding, status: 'detached' }
859
+ : binding),
860
+ pendingOperation: undefined,
861
+ ...(recoveryIssues.length > 0 ? { recoveryIssues } : {}),
862
+ },
863
+ };
864
+ });
865
+ }
866
+ async clearPending(locked, operationId) {
867
+ await locked.mutate((snapshot) => {
868
+ const recoveryIssues = removeRecoveryIssue(snapshot.recoveryIssues, operationId);
869
+ const { repository: _repository, ...withoutRepository } = snapshot;
870
+ void _repository;
871
+ return {
872
+ result: undefined,
873
+ snapshot: {
874
+ ...withoutRepository,
875
+ pendingOperation: undefined,
876
+ ...(recoveryIssues.length > 0 ? { recoveryIssues } : {}),
877
+ },
878
+ };
879
+ });
880
+ }
881
+ async appendRecoveryIssue(locked, issue) {
882
+ await locked.mutate((snapshot) => ({
883
+ result: undefined,
884
+ snapshot: {
885
+ ...snapshot,
886
+ recoveryIssues: [
887
+ ...(snapshot.recoveryIssues ?? []).filter((candidate) => candidate.worktreeId !== issue.worktreeId && candidate.operationId !== issue.operationId),
888
+ issue,
889
+ ],
890
+ },
891
+ }));
892
+ }
893
+ async markRecovery(locked, pending, code) {
894
+ await locked.mutate((snapshot) => ({
895
+ result: undefined,
896
+ snapshot: {
897
+ ...snapshot,
898
+ pendingOperation: { ...pending, phase: 'recovery-needed' },
899
+ recoveryIssues: [
900
+ ...(snapshot.recoveryIssues ?? []).filter((issue) => issue.operationId !== pending.id),
901
+ {
902
+ code,
903
+ operationId: pending.id,
904
+ worktreeId: pending.worktreeId,
905
+ observedAt: new Date().toISOString(),
906
+ },
907
+ ],
908
+ },
909
+ }));
910
+ }
911
+ async reconcileCreateFailure(options) {
912
+ let live;
913
+ try {
914
+ live = await this.git.listWorktrees(options.gitRoot);
915
+ }
916
+ catch (inspectionError) {
917
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
918
+ throw recoveryError(`Unable to inspect Git after create failure: ${options.input.targetPath}`, {
919
+ workspaceId: options.input.workspaceId,
920
+ operationId: options.pending.id,
921
+ targetPath: options.input.targetPath,
922
+ cause: String(inspectionError),
923
+ });
924
+ }
925
+ if (await this.isExactCreatedWorktree(live, options.input)) {
926
+ await this.publishCreated(options.locked, options.pending.id, options.record, options.repository);
927
+ return options.record;
928
+ }
929
+ let branches;
930
+ try {
931
+ branches = await this.git.listBranches(options.gitRoot);
932
+ }
933
+ catch (inspectionError) {
934
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
935
+ throw recoveryError(`Unable to inspect branches after create failure: ${options.input.targetPath}`, {
936
+ workspaceId: options.input.workspaceId,
937
+ operationId: options.pending.id,
938
+ targetPath: options.input.targetPath,
939
+ cause: String(inspectionError),
940
+ });
941
+ }
942
+ const branchAmbiguous = options.pending.type === 'create-worktree' &&
943
+ options.pending.baseRef !== undefined && branches.includes(options.pending.branch);
944
+ let targetExists;
945
+ try {
946
+ targetExists = await pathExists(options.input.targetPath);
947
+ }
948
+ catch (inspectionError) {
949
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
950
+ throw recoveryError(`Unable to inspect the Worktree path after create failure: ${options.input.targetPath}`, {
951
+ workspaceId: options.input.workspaceId,
952
+ operationId: options.pending.id,
953
+ targetPath: options.input.targetPath,
954
+ cause: String(inspectionError),
955
+ });
956
+ }
957
+ if (!branchAmbiguous && !targetExists) {
958
+ await this.clearPending(options.locked, options.pending.id);
959
+ throw normalizeGitError('create worktree', options.input.workspaceRoot, options.input.targetPath, options.error);
960
+ }
961
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
962
+ throw recoveryError(`Git create failed with an unreconciled Worktree: ${options.input.targetPath}`, {
963
+ workspaceId: options.input.workspaceId,
964
+ operationId: options.pending.id,
965
+ targetPath: options.input.targetPath,
966
+ });
967
+ }
968
+ async reconcileRemoveFailure(options) {
969
+ let live;
970
+ try {
971
+ live = await this.git.listWorktrees(options.gitRoot);
972
+ }
973
+ catch (inspectionError) {
974
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
975
+ throw recoveryError(`Unable to inspect Git after remove failure: ${options.record.absolutePath}`, {
976
+ workspaceId: options.input.workspaceId,
977
+ operationId: options.pending.id,
978
+ targetPath: options.record.absolutePath,
979
+ cause: String(inspectionError),
980
+ });
981
+ }
982
+ if (await this.findExactWorktree(live, options.record.absolutePath, options.record.branch)) {
983
+ await this.clearPending(options.locked, options.pending.id);
984
+ throw normalizeGitError('remove worktree', options.input.workspaceRoot, options.record.absolutePath, options.error);
985
+ }
986
+ let targetExists;
987
+ try {
988
+ targetExists = await pathExists(options.record.absolutePath);
989
+ }
990
+ catch (inspectionError) {
991
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_RECOVERY_REQUIRED');
992
+ throw recoveryError(`Unable to inspect the Worktree path after remove failure: ${options.record.absolutePath}`, {
993
+ workspaceId: options.input.workspaceId,
994
+ operationId: options.pending.id,
995
+ targetPath: options.record.absolutePath,
996
+ cause: String(inspectionError),
997
+ });
998
+ }
999
+ if (!targetExists) {
1000
+ await this.publishRemoved(options.locked, options.pending.id, options.record.worktreeId, options.repository);
1001
+ return;
1002
+ }
1003
+ await this.markRecovery(options.locked, options.pending, 'WORKTREE_IDENTITY_CHANGED');
1004
+ throw recoveryError(`Git remove failed with an unreconciled Worktree: ${options.record.absolutePath}`, {
1005
+ workspaceId: options.input.workspaceId,
1006
+ operationId: options.pending.id,
1007
+ targetPath: options.record.absolutePath,
1008
+ });
1009
+ }
1010
+ }
1011
+ //# sourceMappingURL=transaction.js.map