@zq-silk/yui 0.14.0 → 0.14.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/ARCHITECTURE.md +64 -35
  2. package/README.md +258 -125
  3. package/dist/agent/argumentPolicy.js +2 -1
  4. package/dist/agent/managedRuntimeEnvironment.js +5 -8
  5. package/dist/cli/commandCatalog.js +74 -109
  6. package/dist/cli/helpRenderer.js +1 -2
  7. package/dist/cli/interactionCandidates.js +10 -0
  8. package/dist/cli/interactionPolicy.js +76 -37
  9. package/dist/cli/roleWizard.js +5 -6
  10. package/dist/cli/updateOrchestrator.js +10 -8
  11. package/dist/cli/updatePorts.js +17 -13
  12. package/dist/cli/upgradeCommand.js +2 -5
  13. package/dist/cli.js +268 -231
  14. package/dist/commands/agentCommands.js +13 -0
  15. package/dist/commands/configCommands.js +7 -19
  16. package/dist/commands/configOverview.js +3 -2
  17. package/dist/commands/durableJobCommands.js +1 -1
  18. package/dist/commands/executionAuditCommands.js +4 -9
  19. package/dist/commands/globalRoleCommands.js +17 -0
  20. package/dist/commands/profileCommands.js +200 -30
  21. package/dist/commands/sessionCommands.js +1 -1
  22. package/dist/commands/taskActor.js +29 -127
  23. package/dist/commands/taskChangeSetCommands.js +1 -3
  24. package/dist/commands/taskCommands.js +1771 -2107
  25. package/dist/commands/taskCompletionGate.js +27 -103
  26. package/dist/commands/taskContextCommand.js +47 -80
  27. package/dist/commands/taskExecutionCommands.js +9 -27
  28. package/dist/commands/taskInputCommands.js +10 -10
  29. package/dist/commands/taskIntegrationCommands.js +69 -61
  30. package/dist/commands/taskIntegrationQueueCommands.js +1 -1
  31. package/dist/commands/taskNextActionCommand.js +8 -29
  32. package/dist/commands/taskOverviewCommand.js +25 -3
  33. package/dist/commands/taskPublicationCommands.js +151 -85
  34. package/dist/commands/taskPublicationVerifyCommand.js +181 -0
  35. package/dist/commands/taskRemoteDeliveryCommand.js +134 -0
  36. package/dist/commands/taskRoleRuntimeStatus.js +5 -6
  37. package/dist/commands/taskUpstreamCommands.js +79 -95
  38. package/dist/config/configCatalog.js +1 -1
  39. package/dist/context/contextSnapshot.js +17 -2
  40. package/dist/context/sessionBootstrapManifest.js +18 -1
  41. package/dist/context/sourceTurnContext.js +30 -0
  42. package/dist/context/turnContextPack.js +107 -48
  43. package/dist/context/turnInputContract.js +11 -4
  44. package/dist/context/wakeNotification.js +84 -22
  45. package/dist/controller/agentRuntimeObserver.js +9 -12
  46. package/dist/controller/clientRuntime.js +13 -13
  47. package/dist/controller/controller.js +11 -11
  48. package/dist/controller/fileSchedulerStoreAdapter.js +262 -219
  49. package/dist/controller/jobControl.js +11 -10
  50. package/dist/controller/resourceInventory.js +6 -6
  51. package/dist/controller/resourceInventoryLinux.js +2 -2
  52. package/dist/controller/runtime.js +18 -61
  53. package/dist/controller/runtimeEventInbox.js +57 -34
  54. package/dist/controller/runtimeEventProcessor.js +36 -96
  55. package/dist/controller/runtimeHookTurnFence.js +33 -57
  56. package/dist/controller/runtimeLaunchCoordinator.js +44 -40
  57. package/dist/controller/runtimeObservationHook.js +18 -12
  58. package/dist/controller/sessionNotify.js +43 -28
  59. package/dist/controller/sessionOwnerReconciliation.js +21 -21
  60. package/dist/controller/structuredProviderObservation.js +13 -16
  61. package/dist/coordination/workMailboxQueue.js +132 -0
  62. package/dist/core/controllerServer.js +1 -0
  63. package/dist/domain/agentResultTransport.js +60 -0
  64. package/dist/execution/executionHealth.js +111 -118
  65. package/dist/execution/reviewMainTurn.js +161 -0
  66. package/dist/execution/workItemExecution.js +454 -0
  67. package/dist/execution/workItemExecutionProjection.js +309 -0
  68. package/dist/execution/workItemMainTurn.js +164 -0
  69. package/dist/executor/agentExecutor.js +19 -19
  70. package/dist/executor/executorRegistry.js +7 -28
  71. package/dist/executor/fileRoleLaunchPlanner.js +51 -83
  72. package/dist/external/githubPublicationVerifier.js +114 -0
  73. package/dist/external/gitlabPublicationVerifier.js +158 -0
  74. package/dist/external/pinnedCommandRunner.js +73 -0
  75. package/dist/integration/changeSetManifest.js +0 -3
  76. package/dist/integration/deliveryObligation.js +43 -6
  77. package/dist/integration/gitIntegrationService.js +284 -101
  78. package/dist/integration/integrationAttempt.js +64 -14
  79. package/dist/integration/integrationQueueEntry.js +5 -35
  80. package/dist/integration/integrationQueueService.js +65 -194
  81. package/dist/lifecycle/canonicalLifecycleEvent.js +16 -15
  82. package/dist/lifecycle/exactTurnTerminalization.js +100 -97
  83. package/dist/message/message.js +16 -0
  84. package/dist/observability/executionAudit.js +4 -25
  85. package/dist/observability/faultClassification.js +58 -70
  86. package/dist/observability/orchestrationMetrics.js +8 -50
  87. package/dist/profile/agentProfile.js +60 -15
  88. package/dist/profile/agentProfileRuntime.js +100 -0
  89. package/dist/release/releaseWorkflowPorts.js +5 -68
  90. package/dist/repository/executionLaneGitSnapshot.js +80 -0
  91. package/dist/repository/gitWorkspace.js +9 -67
  92. package/dist/repository/taskBaseFreshness.js +3 -1
  93. package/dist/repository/taskWorkspaceCoordinator.js +32 -4
  94. package/dist/repository/taskWorkspacePreparer.js +252 -411
  95. package/dist/resources/liveReferences.js +6 -55
  96. package/dist/resources/resourceDiscovery.js +1 -30
  97. package/dist/resources/resourceRegistrar.js +0 -23
  98. package/dist/review/deltaRecheck.js +8 -65
  99. package/dist/review/reviewAcceptance.js +22 -19
  100. package/dist/review/reviewConfig.js +0 -10
  101. package/dist/review/reviewDecision.js +2 -2
  102. package/dist/review/reviewRound.js +147 -273
  103. package/dist/review/reviewerAvailability.js +13 -23
  104. package/dist/role/role.js +3 -0
  105. package/dist/runtime/agentDriverObservation.js +1 -1
  106. package/dist/runtime/agentHost.js +51 -51
  107. package/dist/runtime/builtinAgentDrivers.js +9 -2
  108. package/dist/runtime/codexAppServerRuntime.js +23 -5
  109. package/dist/runtime/exactControlPlane.js +2 -267
  110. package/dist/runtime/firstProgressAdvisory.js +1 -1
  111. package/dist/runtime/index.js +1 -1
  112. package/dist/runtime/launchBroker.js +11 -11
  113. package/dist/runtime/lifecycleReservation.js +2 -2
  114. package/dist/runtime/managedCaller.js +111 -0
  115. package/dist/runtime/ports.js +16 -3
  116. package/dist/runtime/preallocatedNativeSession.js +2 -2
  117. package/dist/runtime/processExitObservation.js +3 -2
  118. package/dist/runtime/providerContinuationReconciliationService.js +1 -1
  119. package/dist/runtime/runtimeBinding.js +1 -1
  120. package/dist/runtime/runtimeObservation.js +33 -16
  121. package/dist/runtime/runtimeSessionCandidate.js +2 -2
  122. package/dist/runtime/runtimeStopReceipt.js +11 -11
  123. package/dist/runtime/sessionLaunchRequest.js +2 -2
  124. package/dist/runtime/sessionOwnerIdentity.js +9 -9
  125. package/dist/runtime/sessionOwnerRegistry.js +13 -13
  126. package/dist/runtime/sessionReconciliation.js +4 -4
  127. package/dist/runtime/sessionTerminationGuard.js +9 -9
  128. package/dist/runtime/sessionTokenMetrics.js +5 -7
  129. package/dist/runtime/structuredProviderHost.js +5 -5
  130. package/dist/runtime/taskRuntimeIsolation.js +24 -24
  131. package/dist/runtime/tmuxAdapters.js +32 -30
  132. package/dist/scheduler/activeRoleTurnDelivery.js +37 -15
  133. package/dist/scheduler/leaderWakeupProcessor.js +78 -55
  134. package/dist/scheduler/roleTurnLiveness.js +6 -6
  135. package/dist/scheduler/roleTurnStall.js +8 -9
  136. package/dist/scheduler/taskExecutionProjection.js +19 -41
  137. package/dist/scheduler/taskObservabilityProjection.js +75 -122
  138. package/dist/storage/sqliteSchema.js +1 -21
  139. package/dist/storage/sqliteStore.js +62 -47
  140. package/dist/storage/storageSchema.js +1 -8
  141. package/dist/storage/storageVersions.js +7 -4
  142. package/dist/storage/taskStore.js +8 -8
  143. package/dist/storage/upgrade/upgradeOrchestrator.js +112 -83
  144. package/dist/task/completionReadiness.js +17 -98
  145. package/dist/task/deliveryGuard.js +9 -11
  146. package/dist/task/draftPlan.js +142 -0
  147. package/dist/task/nextAction.js +179 -477
  148. package/dist/task/publicationVerification.js +1 -0
  149. package/dist/task/remoteDelivery.js +233 -0
  150. package/dist/task/task.js +108 -4
  151. package/dist/task/taskRecordReference.js +0 -1
  152. package/dist/turn/turn.js +109 -16
  153. package/dist/verification/verificationGateService.js +0 -26
  154. package/dist/web/assets/client/components.js +99 -121
  155. package/dist/web/assets/client/i18n.js +136 -58
  156. package/dist/web/assets/client/view.js +44 -0
  157. package/dist/web/assets/styles/cards.js +3 -15
  158. package/dist/web/assets/styles/widgets.js +4 -0
  159. package/dist/web/webSnapshot.js +29 -14
  160. package/dist/workItem/dependencyGate.js +34 -0
  161. package/dist/workItem/workItem.js +99 -266
  162. package/dist/workspace/workItemChangeSetManager.js +15 -59
  163. package/docs/task-local-identity.md +1 -1
  164. package/i18n/README.zh-CN.md +165 -59
  165. package/package.json +1 -1
  166. package/skills/yui-leader/SKILL.md +272 -782
  167. package/skills/yui-operator/SKILL.md +190 -365
  168. package/skills/yui-reviewer/SKILL.md +40 -33
  169. package/skills/yui-runtime/SKILL.md +27 -6
  170. package/skills/yui-worker/SKILL.md +19 -6
  171. package/dist/execution/candidateConvergence.js +0 -623
  172. package/dist/execution/executionGroup.js +0 -854
  173. package/dist/execution/resourceBroker.js +0 -426
  174. package/dist/executor/launchPlan.js +0 -16
  175. package/dist/review/reviewFinding.js +0 -240
  176. package/dist/review/reviewFindingLedger.js +0 -550
  177. package/dist/review/reviewOutcomeClassifier.js +0 -255
  178. package/dist/scheduler/resourceQueueProjection.js +0 -71
  179. package/dist/task/repairWave.js +0 -150
@@ -1,4 +1,4 @@
1
- import { lstat, mkdir, readlink, readdir, rmdir, symlink, unlink } from "node:fs/promises";
1
+ import { lstat, mkdir, readlink, readdir, rm, rmdir, symlink, unlink } from "node:fs/promises";
2
2
  import { isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { retireTaskRoleSessionsForWorkspace } from "../executor/agentExecutor.js";
@@ -7,14 +7,15 @@ import { updateRole } from "../role/role.js";
7
7
  import { hasRuntimeCleanupObligation, isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
8
  import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
9
9
  import { StorageConflictError } from "../storage/taskStore.js";
10
- import { activateTask, bindTaskWorkspaceIdentity } from "../task/task.js";
10
+ import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits } from "../task/task.js";
11
+ import { validateDraftTaskForActivation } from "../task/draftPlan.js";
11
12
  import { createTaskEvent } from "../event/taskEvent.js";
12
13
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
13
14
  import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
14
15
  import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
15
16
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
16
17
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
17
- import { generateTaskWorkspaceIdentity, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
18
+ import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
18
19
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
19
20
  import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
20
21
  const MAIN_WORKTREE = "main";
@@ -120,6 +121,7 @@ export class FileTaskWorkspacePreparer {
120
121
  changed: false
121
122
  };
122
123
  }
124
+ validateDraftTaskForActivation(this.store, task);
123
125
  const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
124
126
  try {
125
127
  return await this.#prepareTaskWorkspaceLocked(current.id, true);
@@ -190,6 +192,8 @@ export class FileTaskWorkspacePreparer {
190
192
  if (!activate && task.status !== "active") {
191
193
  throw new Error(`Task is not open for workspace preparation: ${task.id}.`);
192
194
  }
195
+ if (activate)
196
+ validateDraftTaskForActivation(this.store, task);
193
197
  const existing = this.store.getTaskWorkspace(task.id);
194
198
  if (activate && (task.workspaceIdentity !== undefined
195
199
  || task.cwd !== undefined
@@ -226,6 +230,8 @@ export class FileTaskWorkspacePreparer {
226
230
  || latest.projectBindings.length !== 0) {
227
231
  throw new Error(`Task changed while preparing its Gitless workspace: ${task.id}.`);
228
232
  }
233
+ if (activate)
234
+ validateDraftTaskForActivation(tx, latest);
229
235
  const current = tx.getTaskWorkspace(task.id);
230
236
  // `createManagedWorkspace` stamps a fresh updatedAt on every call.
231
237
  // Gitless preparation is idempotent, so compare only stable identity
@@ -285,13 +291,12 @@ export class FileTaskWorkspacePreparer {
285
291
  const taskSegment = taskWorkspaceRefSegment(workspaceIdentity === undefined ? task : { ...task, workspaceIdentity });
286
292
  const root = this.#taskWorkspaceRoot(task.id);
287
293
  const prepared = [];
288
- const defaultProjects = remoteDefaultProjects(this.store, task.id);
294
+ const createdClonePaths = new Set();
289
295
  const baselines = new Map();
290
296
  try {
291
- // Resolve every first-use baseline before creating any managed worktree.
292
- // A later Project failure therefore cannot leave an earlier Project's
293
- // workspace behind, and the Task can only be pinned after all evidence
294
- // has been gathered successfully.
297
+ // Resolve every remote baseline before creating any Task clone. A later
298
+ // Project failure therefore cannot leave an earlier Project adopted,
299
+ // and activation remains one all-or-nothing boundary.
295
300
  for (const binding of task.projectBindings) {
296
301
  const project = requireProject(this.store, binding.projectId);
297
302
  const previous = existing?.entries.find(({ projectId }) => projectId === project.id);
@@ -302,37 +307,20 @@ export class FileTaskWorkspacePreparer {
302
307
  });
303
308
  continue;
304
309
  }
305
- const useRemoteDefault = defaultProjects.has(project.id)
306
- && project.remoteUrl !== undefined
307
- && !looksLikeCommit(binding.baseRef);
308
- if (useRemoteDefault) {
309
- const resolver = this.git.resolveRemoteBaseline;
310
- if (typeof resolver !== "function") {
311
- throw new Error(`Git workspace cannot resolve the remote baseline for Project: ${project.id}.`);
312
- }
313
- const remote = await resolver.call(this.git, {
314
- repositoryPath: project.path,
315
- remoteUrl: project.remoteUrl,
316
- // The binding captured the configured development ref at Task
317
- // creation; use that snapshot even if the Project catalog was
318
- // edited before this first workspace preparation.
319
- developmentRef: binding.baseRef
320
- });
321
- baselines.set(project.id, {
322
- baseRef: remote.commit,
323
- recordedBaseRef: remote.commit,
324
- expectedCommit: remote.commit,
325
- pinTask: true,
326
- remoteBaseline: remote
327
- });
328
- continue;
310
+ if (project.remoteUrl === undefined) {
311
+ throw new Error(`Task activation requires a configured remoteUrl for Project: ${project.id}.`);
329
312
  }
330
- // Validate local and explicit refs in the same preflight phase. This
331
- // preserves the fail-closed boundary before any worktree is created.
332
- await this.git.inspect(project.path, binding.baseRef);
313
+ const remote = await this.git.resolveRemoteHead({
314
+ remoteUrl: project.remoteUrl,
315
+ // The binding captures the configured development branch at Draft
316
+ // creation. Project checkout state is intentionally irrelevant.
317
+ branch: binding.baseRef
318
+ });
333
319
  baselines.set(project.id, {
334
- baseRef: binding.baseRef,
335
- recordedBaseRef: binding.baseRef
320
+ baseRef: remote.commit,
321
+ recordedBaseRef: binding.baseRef,
322
+ expectedCommit: remote.commit,
323
+ remoteBaseline: remote
336
324
  });
337
325
  }
338
326
  for (const binding of task.projectBindings) {
@@ -342,15 +330,25 @@ export class FileTaskWorkspacePreparer {
342
330
  if (baseline === undefined) {
343
331
  throw new Error(`Task Project baseline was not resolved: ${project.id}.`);
344
332
  }
345
- const physical = await this.git.ensureWorktree({
346
- repositoryPath: project.path,
347
- container: this.#projectContainer(project.name),
348
- taskSegment,
349
- roleName: MAIN_WORKTREE,
350
- baseRef: previous?.baseCommit ?? baseline.baseRef
351
- });
333
+ const identity = worktreeIdentity(taskSegment, MAIN_WORKTREE);
334
+ const destination = join(this.#projectContainer(project.name), identity.directory);
335
+ const physical = previous === undefined
336
+ ? await this.git.clone({
337
+ remoteUrl: project.remoteUrl,
338
+ destination,
339
+ branch: baseline.remoteBaseline.branch,
340
+ localBranch: identity.branch
341
+ })
342
+ : await this.git.inspect(previous.path, "HEAD");
343
+ if (previous === undefined)
344
+ createdClonePaths.add(physical.root);
345
+ const physicalBranch = await this.git.headRef(physical.root);
346
+ if (physical.root !== (previous?.path ?? destination)
347
+ || physicalBranch !== (previous?.branch ?? identity.branch)) {
348
+ throw new Error(`Task main clone identity changed: ${task.id}/${project.id}.`);
349
+ }
352
350
  if (previous !== undefined
353
- && !await this.git.isAncestor(physical.path, previous.baseCommit, physical.baseCommit)) {
351
+ && !await this.git.isAncestor(previous.path, previous.baseCommit, physical.baseCommit)) {
354
352
  throw new Error(formatWorkspacePreflightError({
355
353
  kind: "physical-drift",
356
354
  reason: `Task main physical HEAD left its recorded lineage: ${task.id}/${project.id}.`,
@@ -363,7 +361,7 @@ export class FileTaskWorkspacePreparer {
363
361
  }
364
362
  if (baseline.expectedCommit !== undefined
365
363
  && physical.baseCommit !== baseline.expectedCommit) {
366
- throw new Error(`Task Project workspace did not start at the fetched remote baseline: ${project.id}.`);
364
+ throw new Error(`Task Project clone did not start at the fetched remote baseline: ${project.id}.`);
367
365
  }
368
366
  const provenance = await captureTaskBaseProvenance({
369
367
  git: this.git,
@@ -379,11 +377,12 @@ export class FileTaskWorkspacePreparer {
379
377
  projectId: project.id,
380
378
  directory: binding.directory,
381
379
  access: "write",
382
- path: physical.path,
383
- branch: physical.branch,
380
+ path: physical.root,
381
+ branch: previous?.branch ?? identity.branch,
384
382
  baseRef: previous?.baseRef ?? baseline.recordedBaseRef,
385
383
  baseCommit: previous?.baseCommit ?? physical.baseCommit
386
384
  },
385
+ currentCommit: physical.baseCommit,
387
386
  provenance
388
387
  });
389
388
  }
@@ -402,6 +401,8 @@ export class FileTaskWorkspacePreparer {
402
401
  if (!isDeepStrictEqual(latest.projectBindings, task.projectBindings)) {
403
402
  throw new Error(`Task Projects changed while preparing its workspace: ${task.id}.`);
404
403
  }
404
+ if (activate)
405
+ validateDraftTaskForActivation(tx, latest);
405
406
  // Revalidate the Project catalog path after acquiring the fence: a
406
407
  // concurrent migration must not leave a persisted worktree whose Git
407
408
  // common dir is the old external checkout. A changed path rides the
@@ -432,16 +433,20 @@ export class FileTaskWorkspacePreparer {
432
433
  : existing === null || !sameManagedWorkspace(current, existing)) {
433
434
  throw new StorageConflictError(`Task workspace changed while preparing its workspace: ${task.id}.`);
434
435
  }
435
- const pinnedBindings = latest.projectBindings.map((binding) => {
436
- const baseline = baselines.get(binding.projectId);
437
- return baseline?.pinTask === true
438
- ? { ...binding, baseRef: baseline.baseRef }
439
- : binding;
440
- });
441
436
  const timestamp = this.now();
442
- let persistedTask = isDeepStrictEqual(pinnedBindings, latest.projectBindings)
443
- ? latest
444
- : { ...latest, projectBindings: pinnedBindings, updatedAt: timestamp.toISOString() };
437
+ let persistedTask = latest;
438
+ if (activate) {
439
+ persistedTask = bindTaskProjectCommits(persistedTask, prepared.map(({ entry }) => ({
440
+ projectId: entry.projectId,
441
+ commit: entry.baseCommit
442
+ })), timestamp);
443
+ }
444
+ else {
445
+ persistedTask = synchronizeTaskProjectCommits(persistedTask, prepared.map(({ entry, currentCommit }) => ({
446
+ projectId: entry.projectId,
447
+ commit: currentCommit
448
+ })), timestamp);
449
+ }
445
450
  if (workspaceIdentity !== undefined) {
446
451
  // The identity is persisted only now that every managed ref was
447
452
  // created successfully. A concurrent prepare that bound the same
@@ -457,8 +462,11 @@ export class FileTaskWorkspacePreparer {
457
462
  tx.saveTask(persistedTask);
458
463
  }
459
464
  tx.saveManagedWorkspace(preserveWorkspaceCreatedAt(workspace, current));
460
- if (current === null) {
461
- recordTaskBaseProvenanceEvents(tx, task.id, prepared.map(({ provenance }) => provenance), timestamp);
465
+ const adoptedProvenance = prepared
466
+ .filter(({ entry }) => (current?.entries.every(({ projectId }) => projectId !== entry.projectId) ?? true))
467
+ .map(({ provenance }) => provenance);
468
+ if (adoptedProvenance.length > 0) {
469
+ recordTaskBaseProvenanceEvents(tx, task.id, adoptedProvenance, timestamp);
462
470
  }
463
471
  for (const role of tx.listRoles(task.id)) {
464
472
  // The Role field is only a cwd/snapshot hint. Preserve it while a
@@ -517,6 +525,15 @@ export class FileTaskWorkspacePreparer {
517
525
  // branches too, so a retry mints a clean identity without half-created
518
526
  // refs behind. Adopted (already catalogued) worktrees are never touched.
519
527
  await this.#discardUnadoptedEntries(task, taskSegment, prepared, MAIN_WORKTREE, true);
528
+ const recorded = new Set(prepared.map(({ entry }) => entry.path));
529
+ for (const path of createdClonePaths) {
530
+ if (recorded.has(path))
531
+ continue;
532
+ const removal = await this.git.removeStrandedWorktree(path);
533
+ if (removal === "dirty") {
534
+ throw new Error(`Unadopted Task clone is dirty and was retained at ${path}; inspect it and retry.`);
535
+ }
536
+ }
520
537
  if (activate)
521
538
  await removeWorkspaceView(root);
522
539
  throw error;
@@ -558,25 +575,11 @@ export class FileTaskWorkspacePreparer {
558
575
  */
559
576
  async #mintTaskWorkspaceIdentity(task) {
560
577
  const home = this.store.getHomeIdentity();
561
- for (let attempt = 0; attempt < 16; attempt += 1) {
562
- const identity = generateTaskWorkspaceIdentity({
563
- home,
564
- taskId: task.id,
565
- now: this.now()
566
- });
567
- const mainBranch = taskMainBranch(taskWorkspaceRefSegmentFromIdentity(identity));
568
- let conflict = false;
569
- for (const binding of task.projectBindings) {
570
- const project = requireProject(this.store, binding.projectId);
571
- if (await this.git.refExists(project.path, mainBranch)) {
572
- conflict = true;
573
- break;
574
- }
575
- }
576
- if (!conflict)
577
- return identity;
578
- }
579
- throw new Error(`Could not mint a unique Task workspace identity for ${task.id}.`);
578
+ return generateTaskWorkspaceIdentity({
579
+ home,
580
+ taskId: task.id,
581
+ now: this.now()
582
+ });
580
583
  }
581
584
  async snapshotCandidateWorkspace(workspace) {
582
585
  if (workspace.owner.type === "review-round") {
@@ -635,34 +638,52 @@ export class FileTaskWorkspacePreparer {
635
638
  }
636
639
  return { schemaVersion: 1, projects };
637
640
  }
641
+ /** Freeze one clean committed source head set before any replicated Lane is minted. */
642
+ async snapshotExecutionLaneInputHeads(workspace, projectIds) {
643
+ if (workspace.owner.type !== "task" && workspace.owner.type !== "work-item") {
644
+ throw new Error("Execution Lane input must come from Task main or a WorkItem workspace.");
645
+ }
646
+ const stored = this.store.getManagedWorkspace(workspace.owner);
647
+ if (stored === null || !isDeepStrictEqual(stored, workspace)) {
648
+ throw new Error("Execution Lane input workspace is not the durable owner.");
649
+ }
650
+ return this.#snapshotWorkspaceHeads(workspace, projectIds, "Execution Lane input");
651
+ }
638
652
  async snapshotDirectTaskMain(workspace, projectIds) {
639
653
  if (workspace.owner.type !== "task") {
640
654
  throw new Error("Only Task main can become a direct Candidate source.");
641
655
  }
656
+ const projects = await this.#snapshotWorkspaceHeads(workspace, projectIds, "Direct Candidate");
657
+ return createDirectTaskMainSnapshot(workspace, projectIds, projects);
658
+ }
659
+ async #snapshotWorkspaceHeads(workspace, projectIds, boundary) {
642
660
  const selected = new Set(projectIds);
661
+ if (selected.size !== projectIds.length) {
662
+ throw new Error(`${boundary} Project scope is duplicated.`);
663
+ }
643
664
  const entries = workspace.entries.filter(({ projectId }) => selected.has(projectId));
644
665
  if (entries.length !== selected.size) {
645
- throw new Error("Direct Candidate Project scope does not match Task main.");
666
+ throw new Error(`${boundary} Project scope does not match its managed workspace.`);
646
667
  }
647
668
  const projects = [];
648
669
  for (const entry of entries) {
649
670
  if (entry.access !== "write") {
650
- throw new Error(`Direct Candidate Project is not writable: ${entry.projectId}.`);
671
+ throw new Error(`${boundary} Project is not writable: ${entry.projectId}.`);
651
672
  }
652
673
  if (!await this.git.isClean(entry.path)) {
653
- throw new Error(`Direct Candidate Task main must be clean and committed: ${entry.projectId}.`);
674
+ throw new Error(`${boundary} workspace must be clean and committed: ${entry.projectId}.`);
654
675
  }
655
676
  const branch = await this.git.headRef(entry.path);
656
677
  if (branch !== entry.branch) {
657
- throw new Error(`Direct Candidate Task main left its managed branch: ${entry.projectId}/${branch}.`);
678
+ throw new Error(`${boundary} workspace left its managed branch: ${entry.projectId}/${branch}.`);
658
679
  }
659
680
  const headCommit = (await this.git.inspect(entry.path, "HEAD")).baseCommit;
660
681
  if (!await this.git.isAncestor(entry.path, entry.baseCommit, headCommit)) {
661
- throw new Error(`Direct Candidate Task main does not descend from its recorded base: ${entry.projectId}.`);
682
+ throw new Error(`${boundary} workspace does not descend from its recorded base: ${entry.projectId}.`);
662
683
  }
663
684
  projects.push({ projectId: entry.projectId, headCommit });
664
685
  }
665
- return createDirectTaskMainSnapshot(workspace, projectIds, projects);
686
+ return projects;
666
687
  }
667
688
  async prepareWorkItemWorkspace(taskId, workItemId) {
668
689
  const item = requireWorkItem(this.store, taskId, workItemId);
@@ -722,9 +743,9 @@ export class FileTaskWorkspacePreparer {
722
743
  : requestedBaseRef ?? head.baseCommit;
723
744
  const requestedBase = previous?.access === "write" || requestedBaseRef === undefined
724
745
  ? null
725
- : await this.git.inspect(project.path, requestedBaseRef);
746
+ : await this.git.inspect(mainEntry.path, requestedBaseRef);
726
747
  const physical = await this.git.ensureWorktree({
727
- repositoryPath: project.path,
748
+ repositoryPath: mainEntry.path,
728
749
  container: this.#projectContainer(project.name),
729
750
  taskSegment,
730
751
  roleName: item.id,
@@ -761,7 +782,7 @@ export class FileTaskWorkspacePreparer {
761
782
  if (requestedBaseRef !== undefined && requestedBaseRef !== previous.baseRef) {
762
783
  throw new Error(`Existing WorkItem Project base ref changed: ${item.id}/${project.id}.`);
763
784
  }
764
- if (!await this.git.isAncestor(project.path, previous.baseCommit, physical.baseCommit)) {
785
+ if (!await this.git.isAncestor(mainEntry.path, previous.baseCommit, physical.baseCommit)) {
765
786
  throw new Error(`Existing WorkItem Project HEAD does not descend from its frozen base: `
766
787
  + `${item.id}/${project.id}.`);
767
788
  }
@@ -842,8 +863,13 @@ export class FileTaskWorkspacePreparer {
842
863
  : { release: () => { }, current: heldFence.current };
843
864
  try {
844
865
  const lineage = executionLaneLineage(this.store, lockedTask, executionGroupId, executionLaneId, hint);
866
+ const item = lineage.purpose === "execution"
867
+ ? this.store.getWorkItem(taskId, lineage.workItemId)
868
+ : null;
845
869
  const source = lineage.purpose === "execution"
846
- ? this.store.getWorkItemWorkspace(taskId, lineage.workItemId)
870
+ ? item?.assignee === LEADER_ROLE
871
+ ? this.store.getTaskWorkspace(taskId)
872
+ : this.store.getWorkItemWorkspace(taskId, lineage.workItemId)
847
873
  : this.store.getReviewRoundWorkspace(taskId, lineage.reviewRoundId);
848
874
  if (source === null) {
849
875
  throw new Error(`Execution Lane source workspace is not ready: ${executionLaneId}.`);
@@ -866,6 +892,16 @@ export class FileTaskWorkspacePreparer {
866
892
  purpose: "review",
867
893
  reviewRoundId: lineage.reviewRoundId
868
894
  };
895
+ const writable = new Set(lineage.purpose === "execution"
896
+ ? item?.writeProjectIds ?? []
897
+ : lockedTask.projectBindings.map(({ projectId }) => projectId));
898
+ const inputHeadByProject = new Map((hint?.inputHeads ?? []).map(({ projectId, headCommit }) => [projectId, headCommit]));
899
+ if (hint?.inputHeads !== undefined
900
+ && (inputHeadByProject.size !== hint.inputHeads.length
901
+ || inputHeadByProject.size !== writable.size
902
+ || [...writable].some((projectId) => !inputHeadByProject.has(projectId)))) {
903
+ throw new Error(`Execution Lane input heads do not match writable scope: ${executionLaneId}.`);
904
+ }
869
905
  const existing = this.store.getManagedWorkspace(owner);
870
906
  if (existing !== null) {
871
907
  if (existing.owner.type !== "execution-lane" || existing.root !== this.#executionLaneWorkspaceRoot(taskId, executionGroupId, executionLaneId)) {
@@ -874,9 +910,13 @@ export class FileTaskWorkspacePreparer {
874
910
  await ensureWorkspaceView(existing.root, existing.entries);
875
911
  this.#registerWorkspace(existing);
876
912
  for (const entry of existing.entries.filter(({ access }) => access === "write")) {
913
+ const inputHead = inputHeadByProject.get(entry.projectId);
914
+ if (hint?.inputHeads !== undefined && inputHead !== entry.baseCommit) {
915
+ throw new Error(`Execution Lane input head changed: ${executionLaneId}/${entry.projectId}.`);
916
+ }
877
917
  const project = requireProject(this.store, entry.projectId);
878
918
  const physical = await this.git.ensureWorktree({
879
- repositoryPath: project.path,
919
+ repositoryPath: this.#taskRepositoryPath(taskId, project.id),
880
920
  container: this.#projectContainer(project.name),
881
921
  taskSegment,
882
922
  roleName: managedWorktreeName(owner),
@@ -888,12 +928,6 @@ export class FileTaskWorkspacePreparer {
888
928
  }
889
929
  return existing;
890
930
  }
891
- const item = lineage.purpose === "execution"
892
- ? this.store.getWorkItem(taskId, lineage.workItemId)
893
- : null;
894
- const writable = new Set(lineage.purpose === "execution"
895
- ? item?.writeProjectIds ?? []
896
- : lockedTask.projectBindings.map(({ projectId }) => projectId));
897
931
  const prepared = [];
898
932
  try {
899
933
  for (const binding of lockedTask.projectBindings) {
@@ -903,13 +937,17 @@ export class FileTaskWorkspacePreparer {
903
937
  prepared.push({ project, entry: { ...sourceEntry, access: "read" } });
904
938
  continue;
905
939
  }
940
+ const inputHead = inputHeadByProject.get(project.id) ?? sourceEntry.baseCommit;
906
941
  const physical = await this.git.ensureWorktree({
907
- repositoryPath: project.path,
942
+ repositoryPath: this.#taskRepositoryPath(taskId, project.id),
908
943
  container: this.#projectContainer(project.name),
909
944
  taskSegment,
910
945
  roleName: managedWorktreeName(owner),
911
- baseRef: sourceEntry.baseCommit
946
+ baseRef: inputHead
912
947
  });
948
+ if (physical.baseCommit !== inputHead) {
949
+ throw new Error(`Execution Lane physical input head changed: ${executionLaneId}/${project.id}.`);
950
+ }
913
951
  prepared.push({
914
952
  project,
915
953
  entry: {
@@ -917,8 +955,8 @@ export class FileTaskWorkspacePreparer {
917
955
  access: "write",
918
956
  path: physical.path,
919
957
  branch: physical.branch,
920
- baseRef: sourceEntry.baseCommit,
921
- baseCommit: sourceEntry.baseCommit
958
+ baseRef: inputHead,
959
+ baseCommit: inputHead
922
960
  }
923
961
  });
924
962
  }
@@ -969,168 +1007,6 @@ export class FileTaskWorkspacePreparer {
969
1007
  }
970
1008
  }
971
1009
  }
972
- /** Merge every selected lane's committed output into the WorkItem owner. */
973
- async materializeExecutionGroupCandidate(taskId, workItemId, executionGroupId, selectedLaneIds) {
974
- const item = requireWorkItem(this.store, taskId, workItemId);
975
- const task = requireTask(this.store, taskId);
976
- const group = workItemExecutionGroupById(item, executionGroupId);
977
- if (group === undefined || group.id !== executionGroupId) {
978
- throw new Error(`ExecutionGroup is not attached to Work Item: ${executionGroupId}.`);
979
- }
980
- const workspace = this.store.getWorkItemWorkspace(taskId, workItemId);
981
- const selected = (selectedLaneIds.length === 0
982
- ? group.lanes.filter((lane) => lane.status === "completed")
983
- : group.lanes.filter((lane) => selectedLaneIds.includes(lane.id)));
984
- if (selected.some((lane) => lane.status !== "completed")) {
985
- throw new Error(`ExecutionGroup has a selected Lane without terminal output: ${executionGroupId}.`);
986
- }
987
- if (selected.length === 0)
988
- throw new Error(`ExecutionGroup has no selected Lane outputs: ${executionGroupId}.`);
989
- // A Gitless Task has no WorkItem workspace to materialize. Its selected
990
- // Lane reports still form a valid non-Git Candidate; no fake workspace or
991
- // commit snapshot is introduced.
992
- if (workspace === null) {
993
- if (task.projectBindings.length === 0
994
- && selected.every((lane) => lane.result?.gitSnapshot === undefined)) {
995
- return { restoreTargets: [] };
996
- }
997
- throw new Error(`Work Item workspace is not ready for Candidate materialization: ${workItemId}.`);
998
- }
999
- if (workspace.owner.type !== "work-item") {
1000
- throw new Error(`Work Item workspace owner is invalid for Candidate materialization: ${workItemId}.`);
1001
- }
1002
- const pendingMerges = new Map();
1003
- const restoreTargets = [];
1004
- const writableProjectIds = workspace.entries
1005
- .filter(({ access }) => access === "write")
1006
- .map(({ projectId }) => projectId);
1007
- if (writableProjectIds.length === 0) {
1008
- for (const lane of selected) {
1009
- if (lane.result?.gitSnapshot !== undefined) {
1010
- throw new Error(`Read-only Lane must not carry a Git snapshot: ${lane.id}.`);
1011
- }
1012
- }
1013
- return {
1014
- workspace,
1015
- // A read-only WorkItem has no Lane Git output, but its frozen
1016
- // WorkItem workspace is still the legitimate Candidate review
1017
- // context. Keep that Candidate snapshot separate from Lane output.
1018
- snapshot: await this.snapshotCandidateWorkspace(workspace),
1019
- restoreTargets: []
1020
- };
1021
- }
1022
- for (const lane of selected) {
1023
- const laneSnapshot = lane.result?.gitSnapshot;
1024
- if (laneSnapshot === undefined) {
1025
- throw new Error(`Selected Lane has no frozen Git snapshot: ${lane.id}.`);
1026
- }
1027
- const snapshotProjectIds = laneSnapshot.projects.map(({ projectId }) => projectId).sort();
1028
- if (!isDeepStrictEqual(snapshotProjectIds, [...writableProjectIds].sort())) {
1029
- throw new Error(`Selected Lane Git snapshot Project scope changed: ${lane.id}.`);
1030
- }
1031
- const laneWorkspace = this.store.getManagedWorkspace({
1032
- type: "execution-lane",
1033
- taskId,
1034
- executionGroupId,
1035
- executionLaneId: lane.id,
1036
- purpose: "execution",
1037
- workItemId
1038
- });
1039
- if (laneWorkspace === null) {
1040
- // fixed(1) retains the WorkItem owner as its single normal-path workspace.
1041
- if (group.lanes.length === 1) {
1042
- for (const target of workspace.entries.filter(({ access }) => access === "write")) {
1043
- const frozen = laneSnapshot.projects.find(({ projectId }) => projectId === target.projectId);
1044
- if (frozen === undefined)
1045
- throw new Error(`Selected Lane snapshot omits Project: ${lane.id}/${target.projectId}.`);
1046
- if (await this.git.headRef(target.path) !== frozen.branch || !await this.git.isClean(target.path)) {
1047
- throw new Error(`Single-Lane workspace is not clean on its managed branch: ${lane.id}/${target.projectId}.`);
1048
- }
1049
- const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
1050
- if (targetHead !== frozen.headCommit) {
1051
- throw new Error(`Single-Lane workspace changed before resolution: ${lane.id}/${target.projectId}.`);
1052
- }
1053
- }
1054
- continue;
1055
- }
1056
- throw new Error(`Selected Execution Lane workspace is not adopted: ${lane.id}.`);
1057
- }
1058
- for (const target of workspace.entries.filter(({ access }) => access === "write")) {
1059
- const source = laneWorkspace.entries.find(({ projectId }) => projectId === target.projectId);
1060
- if (source === undefined)
1061
- throw new Error(`Selected Lane omits Project: ${lane.id}/${target.projectId}.`);
1062
- const frozen = laneSnapshot.projects.find(({ projectId }) => projectId === target.projectId);
1063
- if (frozen === undefined)
1064
- throw new Error(`Selected Lane snapshot omits Project: ${lane.id}/${target.projectId}.`);
1065
- if (await this.git.headRef(source.path) !== frozen.branch || frozen.branch !== source.branch) {
1066
- throw new Error(`Selected Lane left its managed branch: ${lane.id}/${target.projectId}.`);
1067
- }
1068
- if (!await this.git.isClean(source.path)) {
1069
- throw new Error(`Selected Lane has uncommitted output: ${lane.id}/${target.projectId}.`);
1070
- }
1071
- const sourceHead = (await this.git.inspect(source.path, "HEAD")).baseCommit;
1072
- if (sourceHead !== frozen.headCommit) {
1073
- throw new Error(`Selected Lane advanced before resolution: ${lane.id}/${target.projectId}.`);
1074
- }
1075
- const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
1076
- if (sourceHead !== targetHead && !await this.git.isAncestor(target.path, sourceHead, targetHead)) {
1077
- const pending = pendingMerges.get(target.path) ?? [];
1078
- pending.push(frozen.headCommit);
1079
- pendingMerges.set(target.path, pending);
1080
- if (!restoreTargets.some(({ path }) => path === target.path)) {
1081
- restoreTargets.push({ path: target.path, head: targetHead });
1082
- }
1083
- }
1084
- }
1085
- }
1086
- try {
1087
- for (const [targetPath, sourceRefs] of pendingMerges) {
1088
- await this.git.mergeWorktree({ targetPath, sourceRefs });
1089
- const target = restoreTargets.find(({ path }) => path === targetPath);
1090
- if (target !== undefined) {
1091
- target.mergedHead = (await this.git.inspect(targetPath, "HEAD")).baseCommit;
1092
- }
1093
- }
1094
- return {
1095
- workspace,
1096
- snapshot: await this.snapshotCandidateWorkspace(workspace),
1097
- restoreTargets: restoreTargets
1098
- .filter((target) => (target.mergedHead !== undefined && target.mergedHead !== target.head))
1099
- };
1100
- }
1101
- catch (error) {
1102
- for (const target of [...restoreTargets].reverse()) {
1103
- if (target.mergedHead === undefined)
1104
- continue;
1105
- try {
1106
- await this.git.resetWorktree({
1107
- targetPath: target.path,
1108
- expectedHead: target.mergedHead,
1109
- restoreHead: target.head
1110
- });
1111
- }
1112
- catch (restoreError) {
1113
- throw new Error(`Candidate materialization failed and Git compensation could not restore ${target.path}.`, { cause: restoreError });
1114
- }
1115
- }
1116
- throw error;
1117
- }
1118
- }
1119
- async restoreExecutionGroupCandidateMaterialization(materialization) {
1120
- for (const target of [...materialization.restoreTargets].reverse()) {
1121
- const current = (await this.git.inspect(target.path, "HEAD")).baseCommit;
1122
- if (current === target.head)
1123
- continue;
1124
- if (current !== target.mergedHead) {
1125
- throw new Error(`Candidate materialization compensation found unexpected Git head: ${target.path}.`);
1126
- }
1127
- await this.git.resetWorktree({
1128
- targetPath: target.path,
1129
- expectedHead: target.mergedHead,
1130
- restoreHead: target.head
1131
- });
1132
- }
1133
- }
1134
1010
  async cleanupExecutionLaneWorkspace(taskId, executionGroupId, executionLaneId) {
1135
1011
  const task = requireTask(this.store, taskId);
1136
1012
  const lineage = executionLaneLineage(this.store, task, executionGroupId, executionLaneId);
@@ -1141,8 +1017,9 @@ export class FileTaskWorkspacePreparer {
1141
1017
  ? (item === null ? undefined : workItemExecutionGroupById(item, executionGroupId))
1142
1018
  : this.store.getReviewRound(taskId, lineage.reviewRoundId)?.executionGroup;
1143
1019
  const lane = group?.lanes.find(({ id }) => id === executionLaneId);
1020
+ const terminal = lane !== undefined && lane.disposition !== "open";
1144
1021
  if (group === undefined
1145
- || lane === undefined || !["completed", "failed", "skipped"].includes(lane.status)) {
1022
+ || lane === undefined || !terminal) {
1146
1023
  throw new Error(`Execution Lane is not terminally resolved: ${taskId}/${executionLaneId}.`);
1147
1024
  }
1148
1025
  const workspace = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane"
@@ -1150,14 +1027,14 @@ export class FileTaskWorkspacePreparer {
1150
1027
  && owner.executionLaneId === executionLaneId));
1151
1028
  if (workspace === undefined)
1152
1029
  return "missing";
1153
- const state = await this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1030
+ const state = await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1154
1031
  if (state === "dirty")
1155
1032
  return "dirty";
1156
1033
  let removed = false;
1157
1034
  for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
1158
1035
  const project = requireProject(this.store, entry.projectId);
1159
1036
  const result = await this.git.removeWorktree({
1160
- repositoryPath: project.path,
1037
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1161
1038
  container: this.#projectContainer(project.name),
1162
1039
  taskSegment: this.#taskSegment(task),
1163
1040
  roleName: managedWorktreeName(workspace.owner),
@@ -1184,7 +1061,7 @@ export class FileTaskWorkspacePreparer {
1184
1061
  const writable = workspace.entries.filter(({ access }) => access === "write");
1185
1062
  let state;
1186
1063
  try {
1187
- state = await this.#inspectEntries(taskSegment, managedWorktreeName(workspace.owner), writable);
1064
+ state = await this.#inspectEntries(task.id, taskSegment, managedWorktreeName(workspace.owner), writable);
1188
1065
  }
1189
1066
  catch (error) {
1190
1067
  // A `project migrate` between preparation and compensation switches the
@@ -1210,7 +1087,7 @@ export class FileTaskWorkspacePreparer {
1210
1087
  for (const entry of writable) {
1211
1088
  const project = requireProject(this.store, entry.projectId);
1212
1089
  const result = await this.git.removeWorktree({
1213
- repositoryPath: project.path,
1090
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1214
1091
  container: this.#projectContainer(project.name),
1215
1092
  taskSegment,
1216
1093
  roleName: managedWorktreeName(workspace.owner),
@@ -1226,10 +1103,13 @@ export class FileTaskWorkspacePreparer {
1226
1103
  async cleanupExecutionLaneWorkspacesForWorkItem(taskId, workItemId) {
1227
1104
  let result = "missing";
1228
1105
  const item = this.store.getWorkItem(taskId, workItemId);
1229
- for (const group of item?.executionGroups ?? [])
1106
+ const groups = item?.executionGroups ?? [];
1107
+ for (const group of groups)
1230
1108
  for (const lane of group.lanes) {
1231
1109
  const owner = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane" && owner.purpose === "execution"
1232
- && owner.workItemId === workItemId && owner.executionLaneId === lane.id));
1110
+ && owner.workItemId === workItemId
1111
+ && owner.executionGroupId === group.id
1112
+ && owner.executionLaneId === lane.id));
1233
1113
  if (owner !== undefined) {
1234
1114
  if (owner.owner.type !== "execution-lane")
1235
1115
  continue;
@@ -1245,7 +1125,13 @@ export class FileTaskWorkspacePreparer {
1245
1125
  async cleanupExecutionLaneWorkspacesForReviewRound(taskId, reviewRoundId) {
1246
1126
  let result = "missing";
1247
1127
  const round = this.store.getReviewRound(taskId, reviewRoundId);
1248
- for (const lane of round?.executionGroup?.lanes ?? []) {
1128
+ const lanes = round === null
1129
+ ? []
1130
+ : [
1131
+ ...(round.executionGroup?.lanes ?? []),
1132
+ ...(round.executionGroup?.lanes ?? [])
1133
+ ];
1134
+ for (const lane of lanes) {
1249
1135
  const owner = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane" && owner.purpose === "review"
1250
1136
  && owner.reviewRoundId === reviewRoundId && owner.executionLaneId === lane.id));
1251
1137
  if (owner !== undefined) {
@@ -1266,7 +1152,7 @@ export class FileTaskWorkspacePreparer {
1266
1152
  if (workspace === null)
1267
1153
  return "missing";
1268
1154
  assertWorkItemOwnsWorkspace(item, workspace);
1269
- return this.#inspectEntries(item.taskId, managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1155
+ return this.#inspectEntries(item.taskId, this.#taskSegment(requireTask(this.store, item.taskId)), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1270
1156
  }
1271
1157
  /** Prepare a fresh, reviewer-writable workspace from the candidate's
1272
1158
  * frozen Develop snapshot. This record is owned by the ReviewRound; it is
@@ -1398,7 +1284,8 @@ export class FileTaskWorkspacePreparer {
1398
1284
  if (physical.root !== entry.path) {
1399
1285
  throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed path mismatch for ${round.id}/${entry.projectId}.`);
1400
1286
  }
1401
- const projectRoot = await this.git.inspect(project.path, "HEAD");
1287
+ const taskRepository = this.#taskRepositoryPath(task.id, project.id);
1288
+ const projectRoot = await this.git.inspect(taskRepository, "HEAD");
1402
1289
  if (physical.gitDirectory !== projectRoot.gitDirectory) {
1403
1290
  throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace managed Project mismatch for ${round.id}/${entry.projectId}.`);
1404
1291
  }
@@ -1417,7 +1304,7 @@ export class FileTaskWorkspacePreparer {
1417
1304
  }
1418
1305
  let descendsFromBase;
1419
1306
  try {
1420
- descendsFromBase = await this.git.isAncestor(project.path, entry.baseCommit, physical.baseCommit);
1307
+ descendsFromBase = await this.git.isAncestor(taskRepository, entry.baseCommit, physical.baseCommit);
1421
1308
  }
1422
1309
  catch (error) {
1423
1310
  throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace ancestry cannot be verified for `
@@ -1458,7 +1345,7 @@ export class FileTaskWorkspacePreparer {
1458
1345
  for (const source of sources) {
1459
1346
  const project = requireProject(this.store, source.projectId);
1460
1347
  const physical = await this.git.ensureWorktree({
1461
- repositoryPath: project.path,
1348
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1462
1349
  container: this.#projectContainer(project.name),
1463
1350
  taskSegment,
1464
1351
  roleName: this.#reviewWorktreeName(round),
@@ -1480,7 +1367,7 @@ export class FileTaskWorkspacePreparer {
1480
1367
  }
1481
1368
  let descendsFromBase;
1482
1369
  try {
1483
- descendsFromBase = await this.git.isAncestor(project.path, source.baseCommit, physical.baseCommit);
1370
+ descendsFromBase = await this.git.isAncestor(this.#taskRepositoryPath(task.id, project.id), source.baseCommit, physical.baseCommit);
1484
1371
  }
1485
1372
  catch (error) {
1486
1373
  throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound workspace ancestry cannot be verified for `
@@ -1716,75 +1603,7 @@ export class FileTaskWorkspacePreparer {
1716
1603
  if (round.workspace !== undefined && !isDeepStrictEqual(round.workspace, workspace)) {
1717
1604
  throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
1718
1605
  }
1719
- return this.#inspectEntries(this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries);
1720
- }
1721
- async snapshotReviewRoundResult(taskId, reviewRoundId) {
1722
- const round = this.store.getReviewRound(taskId, reviewRoundId);
1723
- if (round === null)
1724
- throw new Error(`ReviewRound not found: ${taskId}/${reviewRoundId}.`);
1725
- const workspace = this.store.getReviewRoundWorkspace(taskId, reviewRoundId);
1726
- if (round.status !== "running" || workspace === null) {
1727
- throw new Error(`ReviewRound is not running in a managed workspace: ${reviewRoundId}.`);
1728
- }
1729
- assertReviewRoundOwnsWorkspace(round.id, workspace);
1730
- if (round.workspace === undefined || !isDeepStrictEqual(round.workspace, workspace)) {
1731
- throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
1732
- }
1733
- return this.#snapshotReviewWorkspaceEntries(round.id, workspace, round.reviewBaseCommit);
1734
- }
1735
- /**
1736
- * Snapshot the exact workspace recorded on one Reviewer Turn. Panel Lanes
1737
- * own separate durable workspaces; the ReviewRound's shared workspace is
1738
- * not a valid substitute for a Lane's branch/head evidence.
1739
- */
1740
- async snapshotReviewRunResult(taskId, run) {
1741
- if (run.taskId !== taskId || run.purpose !== "review" || run.reviewRoundId === undefined) {
1742
- throw new Error(`Turn is not an exact ReviewRound Turn: ${run.id}.`);
1743
- }
1744
- if (run.workspace === undefined) {
1745
- throw new Error(`Review Turn has no managed workspace: ${run.id}.`);
1746
- }
1747
- const round = this.store.getReviewRound(taskId, run.reviewRoundId);
1748
- if (round === null || round.status !== "running") {
1749
- throw new Error(`ReviewRound is not running for Review Turn: ${run.id}.`);
1750
- }
1751
- const stored = this.store.getManagedWorkspace(run.workspace.owner);
1752
- if (stored === null || !isDeepStrictEqual(stored, run.workspace)) {
1753
- throw new Error(`Review Turn workspace is not the durable owner: ${run.id}.`);
1754
- }
1755
- if (run.workspace.owner.type === "review-round") {
1756
- if (run.workspace.owner.taskId !== taskId
1757
- || run.workspace.owner.reviewRoundId !== round.id
1758
- || round.workspace === undefined
1759
- || !isDeepStrictEqual(round.workspace, run.workspace)) {
1760
- throw new Error(`Review Turn workspace owner does not match its ReviewRound: ${run.id}.`);
1761
- }
1762
- return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
1763
- }
1764
- if (run.workspace.owner.type !== "execution-lane"
1765
- || run.workspace.owner.taskId !== taskId
1766
- || run.workspace.owner.purpose !== "review"
1767
- || run.workspace.owner.reviewRoundId !== round.id
1768
- || run.workspace.owner.executionGroupId !== run.executionGroupId
1769
- || run.workspace.owner.executionLaneId !== run.executionLaneId) {
1770
- throw new Error(`Review Turn workspace owner does not match its Lane lineage: ${run.id}.`);
1771
- }
1772
- const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
1773
- const writableProjectIds = run.workspace.entries
1774
- .filter(({ access }) => access === "write")
1775
- .map(({ projectId }) => projectId)
1776
- .sort();
1777
- if (lane === undefined
1778
- || lane.turnId !== run.id
1779
- || lane.roleName !== run.roleName
1780
- || lane.reviewRoundId !== round.id
1781
- || lane.workspace?.root !== run.workspace.root
1782
- || !isDeepStrictEqual([...lane.workspace.writableProjectIds].sort(), writableProjectIds)
1783
- || run.workspace.entries.length === 0
1784
- || run.workspace.entries.some(({ access }) => access !== "write")) {
1785
- throw new Error(`Review Turn Lane workspace lineage is not exact: ${run.id}.`);
1786
- }
1787
- return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
1606
+ return this.#inspectEntries(task.id, this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries);
1788
1607
  }
1789
1608
  async inspectExecutionLaneWorkspace(taskId, executionGroupId, executionLaneId) {
1790
1609
  const task = requireTask(this.store, taskId);
@@ -1793,7 +1612,7 @@ export class FileTaskWorkspacePreparer {
1793
1612
  && owner.executionLaneId === executionLaneId));
1794
1613
  if (workspace === undefined)
1795
1614
  return "missing";
1796
- return this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1615
+ return this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1797
1616
  }
1798
1617
  async cleanupReviewRoundWorkspace(taskId, reviewRoundId) {
1799
1618
  const task = requireTask(this.store, taskId);
@@ -1818,13 +1637,16 @@ export class FileTaskWorkspacePreparer {
1818
1637
  for (const lane of round.executionGroup?.lanes ?? []) {
1819
1638
  assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
1820
1639
  }
1821
- if (await this.#inspectEntries(this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries) === "dirty")
1640
+ for (const lane of round.executionGroup?.lanes ?? []) {
1641
+ assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
1642
+ }
1643
+ if (await this.#inspectEntries(task.id, this.#taskSegment(task), this.#reviewWorktreeName(round), workspace.entries) === "dirty")
1822
1644
  return "dirty";
1823
1645
  let removed = false;
1824
1646
  for (const entry of workspace.entries) {
1825
1647
  const project = requireProject(this.store, entry.projectId);
1826
1648
  const result = await this.git.removeWorktree({
1827
- repositoryPath: project.path,
1649
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1828
1650
  container: this.#projectContainer(project.name),
1829
1651
  taskSegment: this.#taskSegment(task),
1830
1652
  roleName: this.#reviewWorktreeName(round),
@@ -1884,13 +1706,13 @@ export class FileTaskWorkspacePreparer {
1884
1706
  assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
1885
1707
  }
1886
1708
  const writable = workspace.entries.filter(({ access }) => access === "write");
1887
- if (await this.#inspectEntries(this.#taskSegment(task), managedWorktreeName(workspace.owner), writable) === "dirty")
1709
+ if (await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), writable) === "dirty")
1888
1710
  return "dirty";
1889
1711
  let removed = false;
1890
1712
  for (const entry of writable) {
1891
1713
  const project = requireProject(this.store, entry.projectId);
1892
1714
  const result = await this.git.removeWorktree({
1893
- repositoryPath: project.path,
1715
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1894
1716
  container: this.#projectContainer(project.name),
1895
1717
  taskSegment: this.#taskSegment(task),
1896
1718
  roleName: managedWorktreeName(workspace.owner),
@@ -1916,6 +1738,61 @@ export class FileTaskWorkspacePreparer {
1916
1738
  }
1917
1739
  return removed ? "removed" : "missing";
1918
1740
  }
1741
+ async inspectIntegrationWorkspace(taskId, integrationId) {
1742
+ const workspace = this.store.getIntegrationWorkspace(taskId, integrationId);
1743
+ if (workspace === null)
1744
+ return "missing";
1745
+ if (workspace.owner.type !== "integration-attempt"
1746
+ || workspace.owner.integrationAttemptId !== integrationId) {
1747
+ throw new Error(`Integration workspace ownership is invalid: ${taskId}/${integrationId}.`);
1748
+ }
1749
+ let found = false;
1750
+ for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
1751
+ try {
1752
+ if (!await this.git.isClean(entry.path))
1753
+ return "dirty";
1754
+ found = true;
1755
+ }
1756
+ catch (error) {
1757
+ if (error instanceof Error && error.message.includes("No such file"))
1758
+ continue;
1759
+ throw error;
1760
+ }
1761
+ }
1762
+ return found ? "clean" : "missing";
1763
+ }
1764
+ async cleanupIntegrationWorkspace(taskId, integrationId) {
1765
+ const task = requireTask(this.store, taskId);
1766
+ const attempt = this.store.getIntegrationAttempt(task.id, integrationId);
1767
+ if (attempt === null) {
1768
+ throw new Error(`IntegrationAttempt not found: ${task.id}/${integrationId}.`);
1769
+ }
1770
+ if (attempt.status === "running"
1771
+ || attempt.status === "blocked"
1772
+ || attempt.status === "validating") {
1773
+ throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
1774
+ }
1775
+ const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
1776
+ const project = requireProject(this.store, attempt.projectId);
1777
+ const result = await this.git.removeIntegrationWorktree({
1778
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
1779
+ container: this.#projectContainer(project.name),
1780
+ taskSegment: this.#taskSegment(task),
1781
+ integrationId: attempt.id,
1782
+ discardChanges: attempt.status === "failed"
1783
+ });
1784
+ if (result !== "dirty") {
1785
+ if (workspace !== null)
1786
+ this.#resourceRegistrar().markWorkspaceDeleted(workspace);
1787
+ await rm(join(this.home, "artifacts", "integration-checks", task.id, attempt.id), { recursive: true, force: true });
1788
+ this.store.removeManagedWorkspace({
1789
+ type: "integration-attempt",
1790
+ taskId: task.id,
1791
+ integrationAttemptId: attempt.id
1792
+ });
1793
+ }
1794
+ return result;
1795
+ }
1919
1796
  async inspectTaskMainWorkspace(taskId) {
1920
1797
  const task = requireTask(this.store, taskId);
1921
1798
  const main = this.store.getTaskWorkspace(task.id);
@@ -1924,7 +1801,7 @@ export class FileTaskWorkspacePreparer {
1924
1801
  if (main.owner.type !== "task") {
1925
1802
  throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
1926
1803
  }
1927
- return this.#inspectEntries(this.#taskSegment(task), MAIN_WORKTREE, main.entries);
1804
+ return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
1928
1805
  }
1929
1806
  async cleanupTaskForArchive(taskId) {
1930
1807
  const task = requireTask(this.store, taskId);
@@ -1942,7 +1819,7 @@ export class FileTaskWorkspacePreparer {
1942
1819
  const main = this.store.getTaskWorkspace(task.id);
1943
1820
  if (main !== null) {
1944
1821
  assertTaskArchiveState(requireTask(this.store, task.id), task);
1945
- if (await this.#inspectEntries(this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
1822
+ if (await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
1946
1823
  return {
1947
1824
  taskId,
1948
1825
  status: "retained-dirty",
@@ -1951,13 +1828,7 @@ export class FileTaskWorkspacePreparer {
1951
1828
  }
1952
1829
  for (const entry of main.entries) {
1953
1830
  assertTaskArchiveState(requireTask(this.store, task.id), task);
1954
- const project = requireProject(this.store, entry.projectId);
1955
- const result = await this.git.removeWorktree({
1956
- repositoryPath: project.path,
1957
- container: this.#projectContainer(project.name),
1958
- taskSegment: this.#taskSegment(task),
1959
- roleName: MAIN_WORKTREE
1960
- });
1831
+ const result = await this.git.removeStrandedWorktree(entry.path);
1961
1832
  if (result === "dirty") {
1962
1833
  throw new Error(`Task workspace changed after cleanup preflight: ${task.id}.`);
1963
1834
  }
@@ -1971,14 +1842,14 @@ export class FileTaskWorkspacePreparer {
1971
1842
  this.#clearTaskWorkspace(task, this.#fallbackWorkspace());
1972
1843
  return { taskId, status: "removed" };
1973
1844
  }
1974
- async #inspectEntries(taskSegment, roleName, entries) {
1845
+ async #inspectEntries(taskId, taskSegment, roleName, entries) {
1975
1846
  if (entries.length === 0)
1976
1847
  return "missing";
1977
1848
  let found = false;
1978
1849
  for (const entry of entries) {
1979
1850
  const project = requireProject(this.store, entry.projectId);
1980
1851
  const state = await this.git.inspectWorktree({
1981
- repositoryPath: project.path,
1852
+ repositoryPath: this.#taskRepositoryPath(taskId, project.id),
1982
1853
  container: this.#projectContainer(project.name),
1983
1854
  taskSegment,
1984
1855
  roleName
@@ -1989,39 +1860,6 @@ export class FileTaskWorkspacePreparer {
1989
1860
  }
1990
1861
  return found ? "clean" : "missing";
1991
1862
  }
1992
- async #snapshotReviewWorkspaceEntries(reviewRoundId, workspace, reviewBaseCommit) {
1993
- const changed = [];
1994
- let dirty = false;
1995
- for (const entry of workspace.entries) {
1996
- if (await this.git.headRef(entry.path) !== entry.branch) {
1997
- throw new Error(`Review Project workspace left its managed branch: ${reviewRoundId}/${entry.projectId}.`);
1998
- }
1999
- const head = (await this.git.inspect(entry.path, "HEAD")).baseCommit;
2000
- const project = requireProject(this.store, entry.projectId);
2001
- if (!await this.git.isAncestor(project.path, entry.baseCommit, head)) {
2002
- throw new Error(`Review workspace HEAD does not descend from its frozen base for `
2003
- + `${reviewRoundId}/${entry.projectId}: expected ancestor ${entry.baseCommit}, `
2004
- + `physical HEAD ${head}.`);
2005
- }
2006
- if (head !== entry.baseCommit)
2007
- changed.push(head);
2008
- if (!await this.git.isClean(entry.path))
2009
- dirty = true;
2010
- }
2011
- if (changed.length > 1) {
2012
- throw new Error(`Review workspace has diagnostic commits in multiple Projects; preserve it for Leader routing: ${reviewRoundId}.`);
2013
- }
2014
- // A dirty worktree has uncommitted diagnostics: no single commit captures
2015
- // the tree the checks ran on, so no evidenceCommit can attest it and the
2016
- // queue must re-run the gate. A clean worktree attests that checks ran on
2017
- // the recorded tree: the frozen base when the reviewer made no commits, or
2018
- // the reviewer's single diagnostic commit otherwise.
2019
- if (dirty)
2020
- return {};
2021
- return changed.length === 0
2022
- ? { evidenceCommit: reviewBaseCommit }
2023
- : { evidenceCommit: changed[0] };
2024
- }
2025
1863
  #projectContainer(projectName) {
2026
1864
  return join(resolveWorktreeRoot(this.home, this.store.getConfig().defaultWorkspace), safePathSegment(projectName));
2027
1865
  }
@@ -2057,8 +1895,16 @@ export class FileTaskWorkspacePreparer {
2057
1895
  async #discardUnadoptedEntries(task, taskSegment, prepared, roleName, deleteBranch = false, adoptedPaths = new Set(this.store.listManagedWorkspaces(task.id)
2058
1896
  .flatMap((workspace) => workspace.entries.map(({ path }) => path)))) {
2059
1897
  for (const { project, entry } of prepared.filter(({ entry }) => entry.access === "write" && !adoptedPaths.has(entry.path))) {
1898
+ if (roleName === MAIN_WORKTREE) {
1899
+ const removal = await this.git.removeStrandedWorktree(entry.path);
1900
+ if (removal === "dirty") {
1901
+ throw new Error(`Unadopted Task clone is dirty and was retained at ${entry.path}; inspect it and retry.`);
1902
+ }
1903
+ this.#resourceRegistrar().markPathsDeleted([entry.path]);
1904
+ continue;
1905
+ }
2060
1906
  const removal = await this.git.removeWorktree({
2061
- repositoryPath: project.path,
1907
+ repositoryPath: this.#taskRepositoryPath(task.id, project.id),
2062
1908
  container: this.#projectContainer(project.name),
2063
1909
  taskSegment,
2064
1910
  roleName,
@@ -2070,6 +1916,17 @@ export class FileTaskWorkspacePreparer {
2070
1916
  this.#resourceRegistrar().markPathsDeleted([entry.path]);
2071
1917
  }
2072
1918
  }
1919
+ #taskRepositoryPath(taskId, projectId) {
1920
+ const workspace = this.store.getTaskWorkspace(taskId);
1921
+ if (workspace === null || workspace.owner.type !== "task") {
1922
+ throw new Error(`Task main workspace is unavailable: ${taskId}.`);
1923
+ }
1924
+ const entry = workspace.entries.find((candidate) => candidate.projectId === projectId);
1925
+ if (entry === undefined) {
1926
+ throw new Error(`Task main Project workspace is unavailable: ${taskId}/${projectId}.`);
1927
+ }
1928
+ return entry.path;
1929
+ }
2073
1930
  #recordWorkspaceRemoval(task, workspace, fallback, workItem) {
2074
1931
  this.store.transaction((tx) => {
2075
1932
  if (workspace.owner.type === "task") {
@@ -2272,7 +2129,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2272
2129
  || session === undefined
2273
2130
  || session.agentId !== role.activeAgentId
2274
2131
  || session.adapterId !== run.effective.adapterId
2275
- || session.launchId === undefined
2132
+ || session.runtimeGenerationId === undefined
2276
2133
  || session.nativeSessionId === undefined
2277
2134
  || !["ready", "running"].includes(session.status)
2278
2135
  || !isDeepStrictEqual(session.effective, run.effective)
@@ -2290,7 +2147,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2290
2147
  && lifecycle !== undefined
2291
2148
  && isRuntimeLaunchReservation(lifecycle)) {
2292
2149
  const executionRef = lifecycle.executionRef;
2293
- if (!isRuntimeLaunchReservation(lifecycle, session.launchId)
2150
+ if (!isRuntimeLaunchReservation(lifecycle, session.runtimeGenerationId)
2294
2151
  || executionRef?.type !== "turn"
2295
2152
  || executionRef.taskId !== taskId
2296
2153
  || executionRef.id !== run.id)
@@ -2367,22 +2224,6 @@ function safePathSegment(value) {
2367
2224
  function sameCommit(left, right) {
2368
2225
  return left.toLowerCase() === right.toLowerCase();
2369
2226
  }
2370
- function remoteDefaultProjects(store, taskId) {
2371
- const projectIds = new Set();
2372
- for (const event of store.listEvents(taskId)) {
2373
- if (event.type === "task.created") {
2374
- for (const projectId of (event.payload.defaultProjectIds ?? "").split(",")) {
2375
- const normalized = projectId.trim();
2376
- if (normalized.length > 0)
2377
- projectIds.add(normalized);
2378
- }
2379
- }
2380
- }
2381
- return projectIds;
2382
- }
2383
- function looksLikeCommit(value) {
2384
- return /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/iu.test(value);
2385
- }
2386
2227
  export function resolveWorktreeRoot(home, workspace) {
2387
2228
  return join(resolveWorkspaceRoot(home, workspace), "worktree");
2388
2229
  }