@zq-silk/yui 0.13.9 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/ARCHITECTURE.md +69 -428
  2. package/README.md +108 -298
  3. package/dist/agent/managedRuntimeEnvironment.js +2 -2
  4. package/dist/cli/commandCatalog.js +39 -77
  5. package/dist/cli/interactionCandidates.js +4 -4
  6. package/dist/cli/interactionPolicy.js +7 -10
  7. package/dist/cli/invocationRouter.js +1 -1
  8. package/dist/cli/updateCommand.js +6 -27
  9. package/dist/cli/updateOrchestrator.js +63 -484
  10. package/dist/cli/updatePorts.js +15 -333
  11. package/dist/cli/upgradeCommand.js +8 -65
  12. package/dist/cli.js +57 -242
  13. package/dist/commands/configCommands.js +11 -62
  14. package/dist/commands/executionAuditCommands.js +23 -23
  15. package/dist/commands/globalRoleCommands.js +2 -7
  16. package/dist/commands/jobCommands.js +1 -4
  17. package/dist/commands/projectCommands.js +5 -5
  18. package/dist/commands/releaseCommands.js +3 -10
  19. package/dist/commands/resourcesCommands.js +2 -2
  20. package/dist/commands/sessionCommands.js +2 -2
  21. package/dist/commands/taskActor.js +31 -39
  22. package/dist/commands/taskChangeSetCommands.js +5 -12
  23. package/dist/commands/taskCommands.js +464 -1249
  24. package/dist/commands/taskContextCommand.js +35 -36
  25. package/dist/commands/taskExecutionCommands.js +33 -46
  26. package/dist/commands/taskInputCommands.js +20 -42
  27. package/dist/commands/taskNextActionCommand.js +9 -9
  28. package/dist/commands/taskOverviewCommand.js +56 -95
  29. package/dist/commands/taskRoleRuntimeStatus.js +77 -107
  30. package/dist/commands/taskUpstreamCommands.js +3 -3
  31. package/dist/commands/taskWorkspaceCommands.js +7 -79
  32. package/dist/commands/telemetryCommands.js +32 -124
  33. package/dist/commands/workflowCommands.js +3 -3
  34. package/dist/completion/fileCompletionManager.js +1 -1
  35. package/dist/config/configCatalog.js +4 -6
  36. package/dist/config/yuiConfig.js +6 -43
  37. package/dist/context/dispatchContext.js +14 -30
  38. package/dist/context/roleSessionContext.js +2 -2
  39. package/dist/context/sessionBootstrapManifest.js +2 -2
  40. package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
  41. package/dist/context/turnInputContract.js +204 -0
  42. package/dist/context/wakeNotification.js +2 -2
  43. package/dist/controller/agentRuntimeObserver.js +18 -18
  44. package/dist/controller/clientRuntime.js +13 -20
  45. package/dist/controller/controller.js +89 -116
  46. package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
  47. package/dist/controller/jobControl.js +17 -17
  48. package/dist/controller/resourceInventory.js +6 -6
  49. package/dist/controller/resourceInventoryLinux.js +38 -59
  50. package/dist/controller/resourceInventoryRpc.js +3 -3
  51. package/dist/controller/resourceInventoryWorker.js +1 -1
  52. package/dist/controller/runtime.js +46 -53
  53. package/dist/controller/runtimeEventInbox.js +23 -31
  54. package/dist/controller/runtimeEventProcessor.js +15 -15
  55. package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
  56. package/dist/controller/runtimeLaunchCoordinator.js +3 -3
  57. package/dist/controller/runtimeObservationHook.js +11 -8
  58. package/dist/controller/sessionNotify.js +11 -11
  59. package/dist/controller/structuredProviderObservation.js +89 -16
  60. package/dist/coordination/workMailbox.js +309 -579
  61. package/dist/coordination/workMailboxQueue.js +5 -6
  62. package/dist/core/controllerClient.js +1 -91
  63. package/dist/core/controllerServer.js +3 -6
  64. package/dist/doctor/doctor.js +51 -314
  65. package/dist/execution/candidateConvergence.js +3 -3
  66. package/dist/execution/executionGroup.js +32 -33
  67. package/dist/execution/executionHealth.js +36 -38
  68. package/dist/execution/resourceBroker.js +12 -12
  69. package/dist/executor/agentExecutor.js +30 -265
  70. package/dist/executor/effectiveLaunch.js +9 -19
  71. package/dist/executor/executorRegistry.js +43 -17
  72. package/dist/executor/fileRoleLaunchPlanner.js +56 -56
  73. package/dist/executor/launchPlan.js +3 -3
  74. package/dist/executor/workspacePreflightClassification.js +16 -16
  75. package/dist/grant/capabilityGrant.js +20 -25
  76. package/dist/input/inputRequest.js +12 -12
  77. package/dist/integration/changeSet.js +9 -13
  78. package/dist/integration/changeSetManifest.js +1 -1
  79. package/dist/integration/gitIntegrationService.js +2 -2
  80. package/dist/integration/integrationQueueService.js +5 -21
  81. package/dist/integration/overlapDiagnostics.js +2 -2
  82. package/dist/interaction/operatorPresentation.js +1 -1
  83. package/dist/job/jobRunner.js +1 -1
  84. package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
  85. package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
  86. package/dist/message/message.js +6 -6
  87. package/dist/observability/executionAudit.js +74 -86
  88. package/dist/observability/faultClassification.js +7 -24
  89. package/dist/observability/orchestrationMetrics.js +5 -5
  90. package/dist/observability/runtimeIdentity.js +34 -101
  91. package/dist/profile/agentProfile.js +1 -1
  92. package/dist/release/cliHomeReleaseFence.js +3 -3
  93. package/dist/release/releaseWorkflowEngine.js +6 -10
  94. package/dist/release/releaseWorkflowPorts.js +2 -2
  95. package/dist/release/workflowFileLock.js +1 -1
  96. package/dist/repository/checkoutSwap.js +1 -1
  97. package/dist/repository/gitWorkspace.js +0 -13
  98. package/dist/repository/project.js +1 -1
  99. package/dist/repository/projectMaintenanceLock.js +1 -2
  100. package/dist/repository/taskBaseFreshness.js +13 -13
  101. package/dist/repository/taskWorkspaceCoordinator.js +21 -25
  102. package/dist/repository/taskWorkspaceIdentity.js +8 -21
  103. package/dist/repository/taskWorkspacePreparer.js +63 -620
  104. package/dist/resources/autoResourceGc.js +3 -3
  105. package/dist/resources/liveReferences.js +1 -1
  106. package/dist/resources/resourceGc.js +1 -1
  107. package/dist/resources/sqliteResourceRegistry.js +1 -1
  108. package/dist/review/reviewDecision.js +7 -7
  109. package/dist/review/reviewFindingLedger.js +1 -1
  110. package/dist/review/reviewOutcomeClassifier.js +35 -50
  111. package/dist/review/reviewRound.js +14 -25
  112. package/dist/review/reviewerAvailability.js +4 -4
  113. package/dist/review/taskFinalReviewContractResolution.js +41 -0
  114. package/dist/role/role.js +6 -3
  115. package/dist/runtime/agentDriver.js +4 -0
  116. package/dist/runtime/agentHost.js +151 -36
  117. package/dist/runtime/builtinAgentDrivers.js +79 -2
  118. package/dist/runtime/codexAppServerRuntime.js +122 -0
  119. package/dist/runtime/continuationManager.js +3 -5
  120. package/dist/runtime/exactControlPlane.js +17 -42
  121. package/dist/runtime/firstProgressAdvisory.js +1 -1
  122. package/dist/runtime/index.js +2 -2
  123. package/dist/runtime/launchDiagnostics.js +2 -2
  124. package/dist/runtime/lifecycleReservation.js +4 -4
  125. package/dist/runtime/processExitObservation.js +1 -1
  126. package/dist/runtime/processExitOutbox.js +0 -32
  127. package/dist/runtime/promptEnvelope.js +9 -9
  128. package/dist/runtime/providerContinuation.js +2 -2
  129. package/dist/runtime/providerContinuationReconciliationService.js +2 -2
  130. package/dist/runtime/providerRuntimeIdentity.js +76 -24
  131. package/dist/runtime/recentTurnIds.js +38 -0
  132. package/dist/runtime/runtimeContinuationProjection.js +2 -2
  133. package/dist/runtime/runtimeHealthPolicy.js +1 -1
  134. package/dist/runtime/runtimeObservation.js +58 -21
  135. package/dist/runtime/runtimeProjection.js +19 -27
  136. package/dist/runtime/sessionLaunchRequest.js +2 -2
  137. package/dist/runtime/structuredProviderHost.js +177 -12
  138. package/dist/runtime/tmuxAdapters.js +46 -11
  139. package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
  140. package/dist/scheduler/leaderWakeupProcessor.js +147 -445
  141. package/dist/scheduler/operatorEvent.js +4 -6
  142. package/dist/scheduler/resourceQueueProjection.js +71 -0
  143. package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
  144. package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
  145. package/dist/scheduler/taskExecutionProjection.js +99 -99
  146. package/dist/scheduler/taskObservabilityProjection.js +10 -10
  147. package/dist/scheduler/taskWake.js +4 -4
  148. package/dist/scheduler/wakeReason.js +6 -10
  149. package/dist/scheduler/wakeupQueue.js +0 -9
  150. package/dist/setup/setupCommand.js +2 -2
  151. package/dist/storage/currentTaskStore.js +46 -0
  152. package/dist/storage/persistenceWorker.js +1 -1
  153. package/dist/storage/sqliteSchema.js +81 -485
  154. package/dist/storage/sqliteStore.js +206 -365
  155. package/dist/storage/storageSchema.js +1 -15
  156. package/dist/storage/storageVersions.js +8 -9
  157. package/dist/storage/storeRpc.js +10 -34
  158. package/dist/storage/taskStore.js +63 -3415
  159. package/dist/storage/upgrade/recordVersions.js +55 -149
  160. package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
  161. package/dist/task/completionReadiness.js +5 -5
  162. package/dist/task/deliveryGuard.js +10 -10
  163. package/dist/task/nextAction.js +74 -77
  164. package/dist/task/task.js +2 -4
  165. package/dist/task/taskRecordReference.js +3 -3
  166. package/dist/task/taskRecordRetirement.js +1 -1
  167. package/dist/telemetry/sqliteTelemetryStore.js +53 -53
  168. package/dist/telemetry/telemetryConfig.js +14 -14
  169. package/dist/telemetry/telemetryWiring.js +8 -14
  170. package/dist/turn/turn.js +356 -0
  171. package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
  172. package/dist/verification/gateArtifactStore.js +2 -2
  173. package/dist/web/assets/client/app.js +1 -1
  174. package/dist/web/assets/client/components.js +29 -54
  175. package/dist/web/assets/client/i18n.js +17 -37
  176. package/dist/web/assets/client/view.js +37 -37
  177. package/dist/web/assets/shell.js +1 -1
  178. package/dist/web/assets/styles/cards.js +5 -5
  179. package/dist/web/assets/styles/widgets.js +1 -2
  180. package/dist/web/webSnapshot.js +65 -60
  181. package/dist/workItem/workItem.js +11 -11
  182. package/dist/workspace/workItemChangeSetManager.js +1 -1
  183. package/docs/task-local-identity.md +5 -5
  184. package/i18n/README.zh-CN.md +41 -54
  185. package/package.json +1 -1
  186. package/skills/yui-leader/SKILL.md +85 -83
  187. package/skills/yui-operator/SKILL.md +16 -16
  188. package/skills/yui-reviewer/SKILL.md +10 -10
  189. package/skills/yui-runtime/SKILL.md +17 -18
  190. package/skills/yui-worker/SKILL.md +16 -18
  191. package/dist/context/runContextContract.js +0 -162
  192. package/dist/executor/turnCompletion.js +0 -126
  193. package/dist/review/taskFinalReviewContractEvent.js +0 -1
  194. package/dist/review/taskFinalReviewContractRebind.js +0 -367
  195. package/dist/run/agentRun.js +0 -356
  196. package/dist/run/rejectedYieldAttempt.js +0 -221
  197. package/dist/run/runControlRequest.js +0 -50
  198. package/dist/run/yieldReceipt.js +0 -65
  199. package/dist/runtime/turnCompletion.js +0 -3
  200. package/dist/scheduler/actionability.js +0 -323
  201. package/dist/scheduler/activeRoleRunDelivery.js +0 -868
  202. package/dist/scheduler/activeTaskProgress.js +0 -126
  203. package/dist/storage/compatibleTaskStore.js +0 -248
  204. package/dist/storage/migration/baseline.js +0 -78
  205. package/dist/storage/migration/classifier.js +0 -51
  206. package/dist/storage/migration/compatibleCodec.js +0 -53
  207. package/dist/storage/migration/engine.js +0 -147
  208. package/dist/storage/migration/index.js +0 -33
  209. package/dist/storage/migration/planner.js +0 -154
  210. package/dist/storage/migration/productionRegistry.js +0 -3931
  211. package/dist/storage/migration/registry.js +0 -169
  212. package/dist/storage/migration/report.js +0 -54
  213. package/dist/storage/migration/types.js +0 -31
  214. package/dist/storage/upgrade/homeClassification.js +0 -353
  215. package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
  216. package/dist/storage/upgrade/migrationReceipt.js +0 -67
  217. package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
  218. package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
  219. package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
  220. package/dist/storage/upgrade/recordVersionScan.js +0 -176
  221. package/dist/storage/upgrade/sqliteFileSet.js +0 -50
  222. package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
  223. package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
  224. package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
  225. package/dist/storage/upgrade/switchProgress.js +0 -80
  226. package/dist/storage/upgrade/upgradeReceipt.js +0 -161
  227. package/dist/storage/upgradeCoordination.js +0 -187
  228. package/dist/storage/upgradeFence.js +0 -366
  229. package/dist/telemetry/telemetryCompaction.js +0 -252
@@ -12,10 +12,9 @@ import { createTaskEvent } from "../event/taskEvent.js";
12
12
  import { enqueueWork } from "../coordination/workMailboxQueue.js";
13
13
  import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
14
14
  import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
15
- import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
16
15
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
17
16
  import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
18
- import { generateTaskWorkspaceIdentity, isLegacyTaskRef, taskArchiveRef, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, TASK_WORKSPACE_TOKEN_PATTERN, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
17
+ import { generateTaskWorkspaceIdentity, taskMainBranch, taskWorkspaceRefSegment, taskWorkspaceRefSegmentFromIdentity, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
19
18
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
20
19
  import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
21
20
  const MAIN_WORKTREE = "main";
@@ -71,7 +70,7 @@ export class FileTaskWorkspacePreparer {
71
70
  }
72
71
  async prepareTaskWorkspace(taskId) {
73
72
  // The per-Project maintenance fence makes prepare mutually exclusive with
74
- // migrate/rebuild/archive: a concurrent migration must not switch the
73
+ // Project maintenance and Task archive cleanup: a concurrent migration must not switch the
75
74
  // Project catalog to the Home-managed repo while prepare is creating
76
75
  // worktrees from the old external checkout. The fence is acquired on
77
76
  // every conflict retry so the locked set always covers the Task's
@@ -271,10 +270,14 @@ export class FileTaskWorkspacePreparer {
271
270
  if (existing !== null && existing.owner.type !== "task") {
272
271
  throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
273
272
  }
274
- // The durable workspace identity is minted once, only for a Task that has
275
- // never owned a managed Git workspace. A Task with an existing workspace
276
- // record predates the identity (legacy) and keeps its refs until the
277
- // controlled rebuild; a second prepare reuses the persisted identity.
273
+ if (task.workspaceIdentity === undefined
274
+ && existing !== null
275
+ && existing.entries.length > 0) {
276
+ throw new Error(`Task workspace does not use the current identity contract: ${task.id}. `
277
+ + "Preserve the old Home for diagnosis and let the Operator create a new Task.");
278
+ }
279
+ // The durable workspace identity is minted once before the first managed
280
+ // Git workspace is adopted; every later prepare reuses it.
278
281
  const workspaceIdentity = task.workspaceIdentity === undefined
279
282
  && (existing === null || existing.entries.length === 0)
280
283
  ? await this.#mintTaskWorkspaceIdentity(task)
@@ -414,9 +417,9 @@ export class FileTaskWorkspacePreparer {
414
417
  if (current !== null && current.owner.type !== "task") {
415
418
  throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
416
419
  }
417
- // Single-writer CAS, mirroring the rebuild guard: this attempt only
420
+ // Single-writer CAS: this attempt only
418
421
  // commits while the Task is still unbound (or carries the identity it
419
- // started with). A concurrent prepare or rebuild that bound a
422
+ // started with). A concurrent prepare that bound a
420
423
  // different identity wins; this attempt discards its refs and retries
421
424
  // through the StorageConflictError channel.
422
425
  if (workspaceIdentity !== undefined
@@ -462,18 +465,18 @@ export class FileTaskWorkspacePreparer {
462
465
  // durable WorkItem or ReviewRound workspace owns that cwd; Task-main
463
466
  // preparation must not move a Reviewer Session out from under an
464
467
  // active or retained ReviewRound. Other Roles use Task main.
465
- // Prefer the active Run's exact WorkItem for this Role; fall back
466
- // to the first queued WorkItem only when no active Run owns the Role.
467
- const activeRoleRun = tx.getActiveAgentRun(task.id, role.name);
468
- const activeRunItem = activeRoleRun !== null
469
- && activeRoleRun.purpose === "execution"
470
- && activeRoleRun.workItemId !== undefined
471
- ? tx.getWorkItem(task.id, activeRoleRun.workItemId)
468
+ // Prefer the active Turn's exact WorkItem for this Role; fall back
469
+ // to the first queued WorkItem only when no active Turn owns the Role.
470
+ const activeRoleTurn = tx.getActiveTurn(task.id, role.name);
471
+ const activeTurnItem = activeRoleTurn !== null
472
+ && activeRoleTurn.purpose === "execution"
473
+ && activeRoleTurn.workItemId !== undefined
474
+ ? tx.getWorkItem(task.id, activeRoleTurn.workItemId)
472
475
  : null;
473
- const assignedItem = activeRunItem !== null
474
- && activeRunItem.assignee === role.name
475
- && !["completed", "failed", "retired"].includes(activeRunItem.status)
476
- ? activeRunItem
476
+ const assignedItem = activeTurnItem !== null
477
+ && activeTurnItem.assignee === role.name
478
+ && !["completed", "failed", "retired"].includes(activeTurnItem.status)
479
+ ? activeTurnItem
477
480
  : tx.listWorkItems(task.id).find((candidate) => (candidate.assignee === role.name
478
481
  && !["completed", "failed", "retired"]
479
482
  .includes(candidate.status)));
@@ -666,7 +669,7 @@ export class FileTaskWorkspacePreparer {
666
669
  const task = requireTask(this.store, item.taskId);
667
670
  assertWorkItemWorkspaceEligible(this.store, task, item);
668
671
  // Hold the per-Project maintenance fence across the ensure-main-prepared
669
- // and the WorkItem worktree creation, so a concurrent migrate/rebuild
672
+ // and the WorkItem worktree creation, so concurrent Project maintenance
670
673
  // cannot switch the Project catalog between the two. The under-lock Task
671
674
  // snapshot drives every Project read and Git effect below.
672
675
  const { release, current: lockedTask } = this.#acquireTaskProjectMaintenanceLocks(task);
@@ -785,14 +788,14 @@ export class FileTaskWorkspacePreparer {
785
788
  || !isDeepStrictEqual(latestItem.writeProjectIds, item.writeProjectIds)) {
786
789
  throw new Error(`Work item changed while preparing its workspace: ${item.id}.`);
787
790
  }
788
- const activeDevelopRun = tx.listAgentRuns(lockedTask.id)
791
+ const activeDevelopTurn = tx.listTurns(lockedTask.id)
789
792
  .find((run) => run.status === "active" && run.workItemId === item.id);
790
- if (activeDevelopRun !== undefined) {
791
- throw new Error(`Work Item already has an active Develop Run: ${activeDevelopRun.id}.`);
793
+ if (activeDevelopTurn !== undefined) {
794
+ throw new Error(`Work Item already has an active Develop Turn: ${activeDevelopTurn.id}.`);
792
795
  }
793
796
  if (latestItem.assignee !== undefined
794
- && tx.getActiveAgentRun(lockedTask.id, latestItem.assignee) !== null) {
795
- throw new Error(`Role has an active Run: ${lockedTask.id}/${latestItem.assignee}.`);
797
+ && tx.getActiveTurn(lockedTask.id, latestItem.assignee) !== null) {
798
+ throw new Error(`Role has an active Turn: ${lockedTask.id}/${latestItem.assignee}.`);
796
799
  }
797
800
  const existingWorkspace = tx.getWorkItemWorkspace(lockedTask.id, item.id);
798
801
  if (existingWorkspace !== null && (existingWorkspace.owner.type !== "work-item"
@@ -830,7 +833,7 @@ export class FileTaskWorkspacePreparer {
830
833
  const task = requireTask(this.store, taskId);
831
834
  // Hold the per-Project maintenance fence across Lane worktree creation
832
835
  // (both the reuse and the fresh-mint paths), so a concurrent
833
- // migrate/rebuild cannot switch the Project catalog mid-prepare. A
836
+ // Project maintenance cannot switch the Project catalog mid-prepare. A
834
837
  // dispatch preflight that prepares a whole new Group passes the fence it
835
838
  // already holds (one locked boundary across preparation and adoption);
836
839
  // otherwise the fence is acquired here.
@@ -976,9 +979,9 @@ export class FileTaskWorkspacePreparer {
976
979
  }
977
980
  const workspace = this.store.getWorkItemWorkspace(taskId, workItemId);
978
981
  const selected = (selectedLaneIds.length === 0
979
- ? group.lanes.filter((lane) => lane.status === "yielded" || lane.status === "completed")
982
+ ? group.lanes.filter((lane) => lane.status === "completed")
980
983
  : group.lanes.filter((lane) => selectedLaneIds.includes(lane.id)));
981
- if (selected.some((lane) => !["yielded", "completed"].includes(lane.status))) {
984
+ if (selected.some((lane) => lane.status !== "completed")) {
982
985
  throw new Error(`ExecutionGroup has a selected Lane without terminal output: ${executionGroupId}.`);
983
986
  }
984
987
  if (selected.length === 0)
@@ -1045,7 +1048,7 @@ export class FileTaskWorkspacePreparer {
1045
1048
  }
1046
1049
  const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
1047
1050
  if (targetHead !== frozen.headCommit) {
1048
- throw new Error(`Single-Lane workspace changed after yield: ${lane.id}/${target.projectId}.`);
1051
+ throw new Error(`Single-Lane workspace changed before resolution: ${lane.id}/${target.projectId}.`);
1049
1052
  }
1050
1053
  }
1051
1054
  continue;
@@ -1067,7 +1070,7 @@ export class FileTaskWorkspacePreparer {
1067
1070
  }
1068
1071
  const sourceHead = (await this.git.inspect(source.path, "HEAD")).baseCommit;
1069
1072
  if (sourceHead !== frozen.headCommit) {
1070
- throw new Error(`Selected Lane advanced after yield: ${lane.id}/${target.projectId}.`);
1073
+ throw new Error(`Selected Lane advanced before resolution: ${lane.id}/${target.projectId}.`);
1071
1074
  }
1072
1075
  const targetHead = (await this.git.inspect(target.path, "HEAD")).baseCommit;
1073
1076
  if (sourceHead !== targetHead && !await this.git.isAncestor(target.path, sourceHead, targetHead)) {
@@ -1139,7 +1142,7 @@ export class FileTaskWorkspacePreparer {
1139
1142
  : this.store.getReviewRound(taskId, lineage.reviewRoundId)?.executionGroup;
1140
1143
  const lane = group?.lanes.find(({ id }) => id === executionLaneId);
1141
1144
  if (group === undefined
1142
- || lane === undefined || !["completed", "failed", "yielded", "skipped"].includes(lane.status)) {
1145
+ || lane === undefined || !["completed", "failed", "skipped"].includes(lane.status)) {
1143
1146
  throw new Error(`Execution Lane is not terminally resolved: ${taskId}/${executionLaneId}.`);
1144
1147
  }
1145
1148
  const workspace = this.store.listManagedWorkspaces(taskId).find(({ owner }) => (owner.type === "execution-lane"
@@ -1533,8 +1536,8 @@ export class FileTaskWorkspacePreparer {
1533
1536
  || candidateChanged) {
1534
1537
  throw new ReviewRoundWorkspaceEvidenceError(`ReviewRound changed while preparing its workspace: ${round.id}.`);
1535
1538
  }
1536
- if (tx.getActiveAgentRun(task.id, reviewer.name) !== null) {
1537
- throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Run: ${task.id}/${reviewer.name}.`);
1539
+ if (tx.getActiveTurn(task.id, reviewer.name) !== null) {
1540
+ throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Turn: ${task.id}/${reviewer.name}.`);
1538
1541
  }
1539
1542
  const currentWorkspace = tx.getReviewRoundWorkspace(task.id, round.id);
1540
1543
  if (existing !== null
@@ -1599,8 +1602,8 @@ export class FileTaskWorkspacePreparer {
1599
1602
  || previousWorkspace.root !== reviewRoot) {
1600
1603
  throw new ReviewRoundWorkspaceEvidenceError(`Previous Task-final Review workspace cannot be continued: ${previous.id}.`);
1601
1604
  }
1602
- if (this.store.getActiveAgentRun(task.id, reviewer.name) !== null) {
1603
- throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Run: ${task.id}/${reviewer.name}.`);
1605
+ if (this.store.getActiveTurn(task.id, reviewer.name) !== null) {
1606
+ throw new ReviewRoundWorkspaceEvidenceError(`Reviewer Role has an active Turn: ${task.id}/${reviewer.name}.`);
1604
1607
  }
1605
1608
  const expected = new Map(frozenEntries.map((entry) => [entry.projectId, entry]));
1606
1609
  if (previousWorkspace.entries.length !== expected.size) {
@@ -1667,7 +1670,7 @@ export class FileTaskWorkspacePreparer {
1667
1670
  || currentRound.workspace !== undefined
1668
1671
  || !isDeepStrictEqual(currentRound.taskCandidate, round.taskCandidate)
1669
1672
  || tx.getReviewRoundWorkspace(task.id, round.id) !== null
1670
- || tx.getActiveAgentRun(task.id, reviewer.name) !== null) {
1673
+ || tx.getActiveTurn(task.id, reviewer.name) !== null) {
1671
1674
  throw new ReviewRoundWorkspaceEvidenceError(`Task-final Review workspace changed before reassignment: ${previous.id}/${round.id}.`);
1672
1675
  }
1673
1676
  const latestReviewer = tx.getRole(task.id, reviewer.name);
@@ -1730,31 +1733,31 @@ export class FileTaskWorkspacePreparer {
1730
1733
  return this.#snapshotReviewWorkspaceEntries(round.id, workspace, round.reviewBaseCommit);
1731
1734
  }
1732
1735
  /**
1733
- * Snapshot the exact workspace recorded on one Reviewer Run. Panel Lanes
1736
+ * Snapshot the exact workspace recorded on one Reviewer Turn. Panel Lanes
1734
1737
  * own separate durable workspaces; the ReviewRound's shared workspace is
1735
1738
  * not a valid substitute for a Lane's branch/head evidence.
1736
1739
  */
1737
1740
  async snapshotReviewRunResult(taskId, run) {
1738
1741
  if (run.taskId !== taskId || run.purpose !== "review" || run.reviewRoundId === undefined) {
1739
- throw new Error(`Run is not an exact ReviewRound Run: ${run.id}.`);
1742
+ throw new Error(`Turn is not an exact ReviewRound Turn: ${run.id}.`);
1740
1743
  }
1741
1744
  if (run.workspace === undefined) {
1742
- throw new Error(`Review Run has no managed workspace: ${run.id}.`);
1745
+ throw new Error(`Review Turn has no managed workspace: ${run.id}.`);
1743
1746
  }
1744
1747
  const round = this.store.getReviewRound(taskId, run.reviewRoundId);
1745
1748
  if (round === null || round.status !== "running") {
1746
- throw new Error(`ReviewRound is not running for Review Run: ${run.id}.`);
1749
+ throw new Error(`ReviewRound is not running for Review Turn: ${run.id}.`);
1747
1750
  }
1748
1751
  const stored = this.store.getManagedWorkspace(run.workspace.owner);
1749
1752
  if (stored === null || !isDeepStrictEqual(stored, run.workspace)) {
1750
- throw new Error(`Review Run workspace is not the durable owner: ${run.id}.`);
1753
+ throw new Error(`Review Turn workspace is not the durable owner: ${run.id}.`);
1751
1754
  }
1752
1755
  if (run.workspace.owner.type === "review-round") {
1753
1756
  if (run.workspace.owner.taskId !== taskId
1754
1757
  || run.workspace.owner.reviewRoundId !== round.id
1755
1758
  || round.workspace === undefined
1756
1759
  || !isDeepStrictEqual(round.workspace, run.workspace)) {
1757
- throw new Error(`Review Run workspace owner does not match its ReviewRound: ${run.id}.`);
1760
+ throw new Error(`Review Turn workspace owner does not match its ReviewRound: ${run.id}.`);
1758
1761
  }
1759
1762
  return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
1760
1763
  }
@@ -1764,7 +1767,7 @@ export class FileTaskWorkspacePreparer {
1764
1767
  || run.workspace.owner.reviewRoundId !== round.id
1765
1768
  || run.workspace.owner.executionGroupId !== run.executionGroupId
1766
1769
  || run.workspace.owner.executionLaneId !== run.executionLaneId) {
1767
- throw new Error(`Review Run workspace owner does not match its Lane lineage: ${run.id}.`);
1770
+ throw new Error(`Review Turn workspace owner does not match its Lane lineage: ${run.id}.`);
1768
1771
  }
1769
1772
  const lane = round.executionGroup?.lanes.find(({ id }) => id === run.executionLaneId);
1770
1773
  const writableProjectIds = run.workspace.entries
@@ -1772,14 +1775,14 @@ export class FileTaskWorkspacePreparer {
1772
1775
  .map(({ projectId }) => projectId)
1773
1776
  .sort();
1774
1777
  if (lane === undefined
1775
- || lane.runId !== run.id
1778
+ || lane.turnId !== run.id
1776
1779
  || lane.roleName !== run.roleName
1777
1780
  || lane.reviewRoundId !== round.id
1778
1781
  || lane.workspace?.root !== run.workspace.root
1779
1782
  || !isDeepStrictEqual([...lane.workspace.writableProjectIds].sort(), writableProjectIds)
1780
1783
  || run.workspace.entries.length === 0
1781
1784
  || run.workspace.entries.some(({ access }) => access !== "write")) {
1782
- throw new Error(`Review Run Lane workspace lineage is not exact: ${run.id}.`);
1785
+ throw new Error(`Review Turn Lane workspace lineage is not exact: ${run.id}.`);
1783
1786
  }
1784
1787
  return this.#snapshotReviewWorkspaceEntries(round.id, run.workspace, round.reviewBaseCommit);
1785
1788
  }
@@ -1968,302 +1971,6 @@ export class FileTaskWorkspacePreparer {
1968
1971
  this.#clearTaskWorkspace(task, this.#fallbackWorkspace());
1969
1972
  return { taskId, status: "removed" };
1970
1973
  }
1971
- /**
1972
- * Rebuild an eligible legacy Task's managed Git workspace under a fresh
1973
- * workspace identity. Legacy refs are archived (never deleted outright),
1974
- * the new main worktrees start from verified remote SHAs, and the Task
1975
- * record switches only after every Git side effect succeeded, so a crash
1976
- * at any point leaves the old layout usable and the command resumable.
1977
- *
1978
- * A Task that already carries an identity takes the resume path: only the
1979
- * pending legacy archive and old-worktree removal run.
1980
- */
1981
- async rebuildTaskWorkspace(taskId, options = {}) {
1982
- const task = requireTask(this.store, taskId);
1983
- if (task.status !== "active") {
1984
- throw new Error(`Only an active Task can rebuild its adopted workspace: ${task.id}/${task.status}.`);
1985
- }
1986
- // The rebuild holds every touched Project's maintenance fence for its
1987
- // whole duration, so the Controller defers preparation and no other
1988
- // maintenance interleaves. The resume path may clean up legacy
1989
- // worktrees/refs in any Project; the fresh path touches its bound
1990
- // Projects plus any still holding the Task's legacy refs.
1991
- const projectIds = task.workspaceIdentity !== undefined
1992
- ? this.store.listProjects().map(({ id }) => id)
1993
- : [
1994
- ...new Set([
1995
- ...task.projectBindings.map(({ projectId }) => projectId),
1996
- ...(await this.listLegacyTaskRefs(task.id)).map(({ projectId }) => projectId)
1997
- ])
1998
- ];
1999
- const releaseMaintenance = acquireProjectMaintenanceLocks(this.home, projectIds);
2000
- try {
2001
- return await this.#rebuildTaskWorkspaceLocked(task, options);
2002
- }
2003
- finally {
2004
- releaseMaintenance();
2005
- }
2006
- }
2007
- async #rebuildTaskWorkspaceLocked(task, options) {
2008
- if (task.workspaceIdentity !== undefined) {
2009
- // Quick Win (EXE-08): an identity-bearing Task cannot silently "resume"
2010
- // when --latest is requested. The resume branch only cleans legacy
2011
- // refs and reclaims orphans; it never re-pins the baseline. Reporting
2012
- // success here would manufacture a split-brain state where the physical
2013
- // Task branch moves but the binding, ManagedWorkspace, and Run snapshot
2014
- // stay on the old commit. Refuse explicitly and point at the correct
2015
- // tools instead.
2016
- if (options.latestRemote === true) {
2017
- throw new Error(`Task ${task.id} already owns a workspace identity; `
2018
- + "`task rebuild --latest` cannot re-pin its baseline in place. "
2019
- + "Use `yui task base status " + task.id + "` to inspect the current "
2020
- + "binding, ManagedWorkspace, physical HEAD, and Run snapshot. "
2021
- + "For an Active Task, integrate upstream changes through the "
2022
- + "normal Leader-driven integration path; for a Draft Task with no "
2023
- + "execution evidence, recreate the Task workspace.");
2024
- }
2025
- const current = this.store.getTaskWorkspace(task.id);
2026
- if (current !== null && current.owner.type === "task") {
2027
- await ensureWorkspaceView(current.root, current.entries);
2028
- }
2029
- // Remove the legacy worktrees before archiving their refs: a worktree
2030
- // whose checked-out branch was just deleted reports an unborn-branch
2031
- // ("dirty") status and can no longer be removed cleanly.
2032
- await this.#removeLegacyWorktrees(task, this.store.listProjects().map((project) => project.id));
2033
- const archived = await this.#archiveLegacyRefs(task);
2034
- // Reclaim token worktrees/branches a crashed prepare or rebuild left
2035
- // behind without a catalog record, so they do not accumulate.
2036
- await this.#reclaimOrphanedTaskWorktrees(task);
2037
- return { task: requireTask(this.store, task.id), archived, resumed: true };
2038
- }
2039
- assertTaskHasNoEvidence(this.store, task.id);
2040
- const existing = this.store.getTaskWorkspace(task.id);
2041
- if (existing !== null && existing.owner.type !== "task") {
2042
- throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
2043
- }
2044
- if (existing !== null
2045
- && await this.#inspectLegacyTaskEntries(task, existing.entries) === "dirty") {
2046
- throw new Error(`Task workspace is dirty and blocks the rebuild: ${task.id}.`);
2047
- }
2048
- // Reclaim token worktrees/branches a crashed prepare or rebuild left
2049
- // behind without a catalog record, before minting a new identity. A hard
2050
- // crash after `ensureWorktree` but before the Task/catalog transaction
2051
- // leaves the Task unbound; without this reaping the fresh path would
2052
- // report success and leave the orphaned token worktree/branch behind.
2053
- await this.#reclaimOrphanedTaskWorktrees(task);
2054
- // Resolve verified remote SHAs before any Git side effect, mirroring the
2055
- // first-prepare pinning: a remote default Project is fetched and its
2056
- // exact advertised SHA is pinned; a local or explicit ref is validated.
2057
- const defaultProjects = remoteDefaultProjects(this.store, task.id);
2058
- const pins = new Map();
2059
- for (const binding of task.projectBindings) {
2060
- const project = requireProject(this.store, binding.projectId);
2061
- // `--latest` explicitly re-resolves every remote-backed Project. Without
2062
- // it, only a still-symbolic creation default is refreshed; an explicit
2063
- // or previously pinned commit retains the established rebuild behavior.
2064
- const useRemoteDefault = (options.latestRemote === true
2065
- || (defaultProjects.has(project.id) && !looksLikeCommit(binding.baseRef)))
2066
- && project.remoteUrl !== undefined;
2067
- if (useRemoteDefault) {
2068
- const resolver = this.git.resolveRemoteBaseline;
2069
- if (typeof resolver !== "function") {
2070
- throw new Error(`Git workspace cannot resolve the remote baseline for Project: ${project.id}.`);
2071
- }
2072
- const remote = await resolver.call(this.git, {
2073
- repositoryPath: project.path,
2074
- remoteUrl: project.remoteUrl,
2075
- developmentRef: options.latestRemote === true
2076
- ? project.developmentBranch
2077
- : binding.baseRef
2078
- });
2079
- pins.set(project.id, remote.commit);
2080
- }
2081
- else {
2082
- await this.git.inspect(project.path, binding.baseRef);
2083
- }
2084
- }
2085
- const identity = await this.#mintTaskWorkspaceIdentity(task);
2086
- const taskSegment = taskWorkspaceRefSegmentFromIdentity(identity);
2087
- const root = this.#taskWorkspaceRoot(task.id);
2088
- const prepared = [];
2089
- try {
2090
- for (const binding of task.projectBindings) {
2091
- const project = requireProject(this.store, binding.projectId);
2092
- const baseRef = pins.get(project.id) ?? binding.baseRef;
2093
- const physical = await this.git.ensureWorktree({
2094
- repositoryPath: project.path,
2095
- container: this.#projectContainer(project.name),
2096
- taskSegment,
2097
- roleName: MAIN_WORKTREE,
2098
- baseRef
2099
- });
2100
- if (pins.has(project.id) && physical.baseCommit !== pins.get(project.id)) {
2101
- throw new Error(`Task Project workspace did not start at the fetched remote baseline: ${project.id}.`);
2102
- }
2103
- prepared.push({
2104
- project,
2105
- entry: {
2106
- projectId: project.id,
2107
- directory: binding.directory,
2108
- access: "write",
2109
- path: physical.path,
2110
- branch: physical.branch,
2111
- baseRef,
2112
- baseCommit: physical.baseCommit
2113
- }
2114
- });
2115
- }
2116
- await ensureWorkspaceView(root, prepared.map(({ entry }) => entry));
2117
- const workspace = createManagedWorkspace({
2118
- owner: { type: "task", taskId: task.id },
2119
- root,
2120
- entries: prepared.map(({ entry }) => entry)
2121
- }, this.now());
2122
- this.#registerWorkspace(workspace);
2123
- // Switch the durable record only now that every new ref/worktree exists.
2124
- this.store.transaction((tx) => {
2125
- const latest = requireTask(tx, task.id);
2126
- if (!["draft", "active"].includes(latest.status)
2127
- || latest.workspaceIdentity !== undefined
2128
- || !isDeepStrictEqual(latest.projectBindings, task.projectBindings)) {
2129
- throw new Error(`Task changed while rebuilding its workspace: ${task.id}.`);
2130
- }
2131
- assertTaskHasNoEvidence(tx, task.id);
2132
- const current = tx.getTaskWorkspace(task.id);
2133
- if (current === null
2134
- ? existing !== null
2135
- : existing === null || !sameManagedWorkspace(current, existing)) {
2136
- throw new Error(`Task workspace changed while rebuilding: ${task.id}.`);
2137
- }
2138
- const pinnedBindings = latest.projectBindings.map((binding) => {
2139
- const pinned = pins.get(binding.projectId);
2140
- return pinned !== undefined ? { ...binding, baseRef: pinned } : binding;
2141
- });
2142
- const timestamp = this.now();
2143
- let persistedTask = isDeepStrictEqual(pinnedBindings, latest.projectBindings)
2144
- ? latest
2145
- : { ...latest, projectBindings: pinnedBindings, updatedAt: timestamp.toISOString() };
2146
- persistedTask = bindTaskWorkspaceIdentity(persistedTask, identity, timestamp);
2147
- if (persistedTask.cwd !== root) {
2148
- persistedTask = { ...persistedTask, cwd: root, updatedAt: timestamp.toISOString() };
2149
- }
2150
- if (!isDeepStrictEqual(persistedTask, latest)) {
2151
- tx.saveTask(persistedTask);
2152
- }
2153
- if (existing !== null)
2154
- tx.removeManagedWorkspace(existing.owner);
2155
- tx.saveManagedWorkspace(workspace);
2156
- });
2157
- // Record switched: retire the legacy layout. The worktrees leave first
2158
- // (their branches are retained), then the refs are archived and
2159
- // deleted; each step is resumable.
2160
- await this.#removeLegacyWorktrees(task, existing === null
2161
- ? this.store.listProjects().map((project) => project.id)
2162
- : existing.entries.map(({ projectId }) => projectId));
2163
- const archived = await this.#archiveLegacyRefs(task, existing === null ? undefined : [...new Set(existing.entries.map(({ projectId }) => projectId))]);
2164
- return { task: requireTask(this.store, task.id), archived, resumed: false };
2165
- }
2166
- catch (error) {
2167
- await this.#discardUnadoptedEntries(task, taskSegment, prepared, MAIN_WORKTREE, true);
2168
- throw error;
2169
- }
2170
- }
2171
- /**
2172
- * List legacy (pre-identity) Task refs across the Home's Project
2173
- * repositories, optionally restricted to one Task. Identity-bearing
2174
- * branches (`yui/task-N-<8hex>/...`) are never legacy.
2175
- */
2176
- async listLegacyTaskRefs(taskId) {
2177
- const found = [];
2178
- for (const project of this.store.listProjects()) {
2179
- let refs;
2180
- try {
2181
- refs = await this.git.listRefs(project.path, "refs/heads/yui/");
2182
- }
2183
- catch (error) {
2184
- // A deleted external checkout has no refs to scan; skip it.
2185
- if (isMissingPath(error))
2186
- continue;
2187
- throw error;
2188
- }
2189
- for (const ref of refs) {
2190
- if (!isLegacyTaskRef(ref))
2191
- continue;
2192
- const ownerTaskId = ref.slice("refs/heads/yui/".length).split("/")[0];
2193
- if (taskId !== undefined && ownerTaskId !== taskId)
2194
- continue;
2195
- found.push({ projectId: project.id, taskId: ownerTaskId, ref });
2196
- }
2197
- }
2198
- return found;
2199
- }
2200
- /**
2201
- * Archive legacy Task refs into the Home-scoped archive namespace
2202
- * (`refs/yui/archive/<homeId>/heads/...`). Refs owned by a draft/active
2203
- * Task are refused: their worktrees may still be live. Terminal and
2204
- * unknown-owner refs are archived; an already-archived or missing ref is
2205
- * simply absent on retry.
2206
- *
2207
- * Every registered worktree on a to-be-archived ref is preflighted before
2208
- * the first side effect: a dirty or unidentifiable one fails the whole
2209
- * archive with nothing created, deleted, or removed. A same-repo worktree
2210
- * this Home does not manage that still has the ref checked out fails the
2211
- * archive closed as well, so deleting the ref cannot strand it.
2212
- */
2213
- async archiveLegacyTaskRefs(taskId) {
2214
- const home = this.store.getHomeIdentity();
2215
- const refused = [];
2216
- const archived = [];
2217
- // The pre-lock scan discovers ONLY the sorted Project lock set. The pending
2218
- // set is rebuilt under the fence (below) by re-listing refs and
2219
- // re-classifying their owners, so a Task that transitions terminal ->
2220
- // active between the scan and the lock is refused rather than archived.
2221
- const scanned = await this.listLegacyTaskRefs(taskId);
2222
- const projectIds = [...new Set(scanned.map(({ projectId }) => projectId))].sort();
2223
- // The fence covers preflight and archive alike: a concurrent rebuild
2224
- // must not remove a worktree between its inspection and its ref's
2225
- // archival.
2226
- const releaseMaintenance = projectIds.length === 0
2227
- ? () => { }
2228
- : acquireProjectMaintenanceLocks(this.home, projectIds);
2229
- try {
2230
- const locked = new Set(projectIds);
2231
- const pending = [];
2232
- for (const entry of await this.listLegacyTaskRefs(taskId)) {
2233
- if (!locked.has(entry.projectId))
2234
- continue; // added after the pre-lock scan; not fenced.
2235
- const owner = this.store.getTask(entry.taskId);
2236
- if (owner !== null && ["draft", "active"].includes(owner.status)) {
2237
- refused.push(`${entry.projectId}:${entry.ref}`);
2238
- continue;
2239
- }
2240
- pending.push({
2241
- project: requireProject(this.store, entry.projectId),
2242
- taskId: entry.taskId,
2243
- ref: entry.ref,
2244
- archiveRef: taskArchiveRef(home.homeId, entry.ref)
2245
- });
2246
- }
2247
- for (const target of pending) {
2248
- await this.#assertLegacyRefWorktreeArchivable(target);
2249
- }
2250
- for (const target of pending) {
2251
- // Re-validate under the fence: a Task reopened after classification
2252
- // but before this point must not have its ref deleted.
2253
- const current = this.store.getTask(target.taskId);
2254
- if (current !== null && ["draft", "active"].includes(current.status)) {
2255
- refused.push(`${target.project.id}:${target.ref}`);
2256
- continue;
2257
- }
2258
- await this.#archiveLegacyRef(target);
2259
- archived.push(`${target.project.id}:${target.ref}`);
2260
- }
2261
- return { archived, refused };
2262
- }
2263
- finally {
2264
- releaseMaintenance();
2265
- }
2266
- }
2267
1974
  async #inspectEntries(taskSegment, roleName, entries) {
2268
1975
  if (entries.length === 0)
2269
1976
  return "missing";
@@ -2363,225 +2070,6 @@ export class FileTaskWorkspacePreparer {
2363
2070
  this.#resourceRegistrar().markPathsDeleted([entry.path]);
2364
2071
  }
2365
2072
  }
2366
- /**
2367
- * Archive one Task's legacy refs (`refs/heads/yui/<taskId>/...`) into the
2368
- * Home-scoped archive namespace across the given Project repositories. When
2369
- * no Project list is given, every Project in the Home is scanned, so the
2370
- * resume path also finds refs in Projects that left the Task binding.
2371
- */
2372
- async #archiveLegacyRefs(task, projectIds) {
2373
- const home = this.store.getHomeIdentity();
2374
- const projects = projectIds === undefined
2375
- ? this.store.listProjects()
2376
- : projectIds.map((id) => requireProject(this.store, id));
2377
- const seen = new Set();
2378
- const archived = [];
2379
- for (const project of projects) {
2380
- let refs;
2381
- try {
2382
- refs = await this.git.listRefs(project.path, `refs/heads/yui/${task.id}/`);
2383
- }
2384
- catch (error) {
2385
- // A deleted external checkout has no refs to archive; skip it.
2386
- if (isMissingPath(error))
2387
- continue;
2388
- throw error;
2389
- }
2390
- for (const ref of refs) {
2391
- const ownerTaskId = ref.slice("refs/heads/yui/".length).split("/")[0];
2392
- if (ownerTaskId !== task.id || !isLegacyTaskRef(ref))
2393
- continue;
2394
- const key = `${project.id}:${ref}`;
2395
- if (seen.has(key))
2396
- continue;
2397
- seen.add(key);
2398
- await this.#archiveLegacyRef({
2399
- project,
2400
- taskId: task.id,
2401
- ref,
2402
- archiveRef: taskArchiveRef(home.homeId, ref)
2403
- });
2404
- archived.push(key);
2405
- }
2406
- }
2407
- return archived;
2408
- }
2409
- /**
2410
- * The recorded worktree of a legacy Task ref, when the ref is the Task's
2411
- * main branch. The legacy layout registers exactly one worktree per Task
2412
- * and Project (`<taskId>/main` on `yui/<taskId>/main`); every other legacy
2413
- * ref has no registered worktree and is archived directly.
2414
- */
2415
- #legacyRefWorktree(target) {
2416
- const identity = worktreeIdentity(target.taskId, MAIN_WORKTREE);
2417
- if (target.ref !== `refs/heads/${identity.branch}`)
2418
- return undefined;
2419
- const container = this.#projectContainer(target.project.name);
2420
- return {
2421
- repositoryPath: target.project.path,
2422
- container,
2423
- path: join(container, target.taskId, MAIN_WORKTREE),
2424
- branch: identity.branch,
2425
- taskSegment: target.taskId,
2426
- roleName: MAIN_WORKTREE
2427
- };
2428
- }
2429
- /**
2430
- * Preflight the worktree of a to-be-archived ref: a registered worktree
2431
- * must be clean (or absent) before the archive may touch any ref. An
2432
- * unidentifiable worktree makes `inspectRecordedWorktree` throw, which
2433
- * likewise fails the archive closed.
2434
- */
2435
- async #assertLegacyRefWorktreeArchivable(target) {
2436
- const worktree = this.#legacyRefWorktree(target);
2437
- if (worktree === undefined)
2438
- return;
2439
- const state = await this.git.inspectRecordedWorktree(worktree);
2440
- if (state === "dirty") {
2441
- throw new Error(`Legacy Task worktree is dirty and blocks the archive: ${target.taskId}/${target.project.id}.`);
2442
- }
2443
- }
2444
- /**
2445
- * Archive one legacy ref after removing its registered worktree. The
2446
- * worktree leaves first (its commit retained in the archive ref), then the
2447
- * active ref is deleted; a dirty worktree fails the ref closed. Each step
2448
- * is resumable: a same-commit archive ref resumes, a missing worktree and
2449
- * an already-deleted source are no-ops.
2450
- *
2451
- * Before any mutation, a same-repo worktree this Home does not manage that
2452
- * still has the ref checked out fails the archive closed: `archiveRef`
2453
- * deletes the ref with `update-ref -d`, which bypasses git's
2454
- * worktree-occupancy check and would strand that worktree with an unborn
2455
- * HEAD. The recorded worktree removed below is excluded from the check.
2456
- */
2457
- async #archiveLegacyRef(target) {
2458
- const worktree = this.#legacyRefWorktree(target);
2459
- await this.git.assertNoForeignWorktreeOnRef({
2460
- repositoryPath: target.project.path,
2461
- ref: target.ref,
2462
- excludeWorktreePath: worktree?.path
2463
- });
2464
- if (worktree !== undefined) {
2465
- const removal = await this.git.removeRecordedWorktree({
2466
- ...worktree,
2467
- retainedRef: target.archiveRef
2468
- });
2469
- if (removal === "dirty") {
2470
- throw new Error(`Legacy Task worktree is dirty and blocks the archive: ${target.taskId}/${target.project.id}.`);
2471
- }
2472
- this.#resourceRegistrar().markPathsDeleted([worktree.path]);
2473
- }
2474
- await this.git.archiveRef({
2475
- repositoryPath: target.project.path,
2476
- sourceRef: target.ref,
2477
- archiveRef: target.archiveRef
2478
- });
2479
- }
2480
- /**
2481
- * Remove the legacy worktrees of a Task (the bare `<taskId>/main` layout).
2482
- * A missing worktree is expected on retry and ignored; a dirty one blocks.
2483
- */
2484
- async #removeLegacyWorktrees(task, projectIds) {
2485
- for (const projectId of projectIds) {
2486
- const project = requireProject(this.store, projectId);
2487
- const container = this.#projectContainer(project.name);
2488
- const branch = worktreeIdentity(task.id, MAIN_WORKTREE).branch;
2489
- const removal = await this.git.removeRecordedWorktree({
2490
- repositoryPath: project.path,
2491
- container,
2492
- path: join(container, task.id, MAIN_WORKTREE),
2493
- branch,
2494
- retainedRef: taskArchiveRef(this.store.getHomeIdentity().homeId, `refs/heads/${branch}`),
2495
- taskSegment: task.id,
2496
- roleName: MAIN_WORKTREE
2497
- });
2498
- if (removal === "dirty") {
2499
- throw new Error(`Legacy Task worktree is dirty and blocks the rebuild: ${task.id}/${project.id}.`);
2500
- }
2501
- this.#resourceRegistrar().markPathsDeleted([join(container, task.id, MAIN_WORKTREE)]);
2502
- }
2503
- }
2504
- /**
2505
- * Reclaim token worktrees of this Task that no catalog record owns. A
2506
- * crashed prepare or rebuild (SIGKILL before the catalog transaction) leaves
2507
- * a token-bearing worktree and branch behind; a retry mints a fresh token,
2508
- * so the old one is orphaned. The resume path scans this Home's project
2509
- * containers for `task-N-<token>` directories and removes any the catalog
2510
- * no longer owns, reusing the recorded-worktree mechanism: prove the exact
2511
- * identity, retain the commit in the Home archive, remove the worktree, then
2512
- * archive and delete its now-unchecked-out branch.
2513
- *
2514
- * Only this Home's project containers are scanned, and every candidate must
2515
- * pass the exact recorded-worktree proof (its branch retained in this
2516
- * Home's Project repository) before it is removed. A deleted external
2517
- * checkout orphans the worktree directory, which `removeRecordedWorktree`
2518
- * removes outright; its branch is gone with the repository and needs no
2519
- * further action.
2520
- */
2521
- async #reclaimOrphanedTaskWorktrees(task) {
2522
- const cataloged = new Set(this.store.listManagedWorkspaces(task.id)
2523
- .flatMap((workspace) => workspace.entries.map(({ path }) => path)));
2524
- const homeId = this.store.getHomeIdentity().homeId;
2525
- for (const project of this.store.listProjects()) {
2526
- const container = this.#projectContainer(project.name);
2527
- for (const segment of await listTaskTokenSegments(container, task.id)) {
2528
- const worktreePath = join(container, segment, MAIN_WORKTREE);
2529
- if (cataloged.has(worktreePath))
2530
- continue;
2531
- const branch = worktreeIdentity(segment, MAIN_WORKTREE).branch;
2532
- const archiveRef = taskArchiveRef(homeId, `refs/heads/${branch}`);
2533
- const removal = await this.git.removeRecordedWorktree({
2534
- repositoryPath: project.path,
2535
- container,
2536
- path: worktreePath,
2537
- branch,
2538
- retainedRef: archiveRef,
2539
- taskSegment: segment,
2540
- roleName: MAIN_WORKTREE
2541
- });
2542
- if (removal === "dirty") {
2543
- throw new Error(`Orphaned Task worktree is dirty and blocks the rebuild: ${task.id}/${project.id}.`);
2544
- }
2545
- this.#resourceRegistrar().markPathsDeleted([worktreePath]);
2546
- // The worktree is gone; archive+delete its now-unchecked-out branch.
2547
- // A deleted external checkout takes the branch with it, so a missing
2548
- // repository or an already-absent branch is a no-op.
2549
- let branchExists = false;
2550
- try {
2551
- branchExists = await this.git.refExists(project.path, `refs/heads/${branch}`);
2552
- }
2553
- catch (error) {
2554
- if (!isMissingPath(error))
2555
- throw error;
2556
- }
2557
- if (branchExists) {
2558
- await this.git.archiveRef({
2559
- repositoryPath: project.path,
2560
- sourceRef: `refs/heads/${branch}`,
2561
- archiveRef
2562
- });
2563
- }
2564
- }
2565
- }
2566
- }
2567
- async #inspectLegacyTaskEntries(task, entries) {
2568
- let found = false;
2569
- for (const entry of entries) {
2570
- const project = requireProject(this.store, entry.projectId);
2571
- const state = await this.git.inspectRecordedWorktree({
2572
- repositoryPath: project.path,
2573
- container: this.#projectContainer(project.name),
2574
- path: entry.path,
2575
- branch: entry.branch,
2576
- taskSegment: task.id,
2577
- roleName: MAIN_WORKTREE
2578
- });
2579
- if (state === "dirty")
2580
- return state;
2581
- found ||= state === "clean";
2582
- }
2583
- return found ? "clean" : "missing";
2584
- }
2585
2073
  #recordWorkspaceRemoval(task, workspace, fallback, workItem) {
2586
2074
  this.store.transaction((tx) => {
2587
2075
  if (workspace.owner.type === "task") {
@@ -2649,19 +2137,6 @@ function assertTaskArchiveState(current, expected) {
2649
2137
  throw new WorkspaceCleanupBlockedError("task-changed", `task:${expected.id}`, true, `Task changed during archive cleanup: ${expected.id}.`);
2650
2138
  }
2651
2139
  }
2652
- /**
2653
- * A controlled rebuild is only safe for a Task that owns no delivery
2654
- * evidence: a Run, WorkItem, ChangeSet, or Integration would pin the old
2655
- * refs and worktrees as historical proof.
2656
- */
2657
- function assertTaskHasNoEvidence(store, taskId) {
2658
- if (store.listAgentRuns(taskId).length > 0
2659
- || store.listWorkItems(taskId).length > 0
2660
- || store.listChangeSets(taskId).length > 0
2661
- || store.listIntegrationAttempts(taskId).length > 0) {
2662
- throw new Error(`Task has Run, Work item, Change set, or Integration evidence and cannot be rebuilt in place: ${taskId}.`);
2663
- }
2664
- }
2665
2140
  function requireProject(store, projectId) {
2666
2141
  const project = store.getProject(projectId);
2667
2142
  if (project === null)
@@ -2690,9 +2165,9 @@ function executionLaneLineage(store, task, executionGroupId, executionLaneId, hi
2690
2165
  throw new Error(`ReviewRound not found: ${hint.reviewRoundId}.`);
2691
2166
  return { purpose: "review", reviewRoundId: round.id };
2692
2167
  }
2693
- // Prefer the active Run's exact WorkItem for this Lane; fall back to the
2694
- // first queued WorkItem only when no active Run owns the Lane.
2695
- const activeLaneRun = store.listAgentRuns(task.id)
2168
+ // Prefer the active Turn's exact WorkItem for this Lane; fall back to the
2169
+ // first queued WorkItem only when no active Turn owns the Lane.
2170
+ const activeLaneRun = store.listTurns(task.id)
2696
2171
  .find((run) => run.status === "active"
2697
2172
  && run.purpose === "execution"
2698
2173
  && run.executionGroupId === executionGroupId
@@ -2720,13 +2195,13 @@ function assertWorkItemWorkspaceEligible(store, task, item) {
2720
2195
  if (["completed", "failed", "retired"].includes(item.status)) {
2721
2196
  throw new Error(`Work item is already terminal: ${item.id}.`);
2722
2197
  }
2723
- const activeDevelopRun = store.listAgentRuns(task.id)
2198
+ const activeDevelopTurn = store.listTurns(task.id)
2724
2199
  .find((run) => run.status === "active" && run.workItemId === item.id);
2725
- if (activeDevelopRun !== undefined) {
2726
- throw new Error(`Work Item already has an active Develop Run: ${activeDevelopRun.id}.`);
2200
+ if (activeDevelopTurn !== undefined) {
2201
+ throw new Error(`Work Item already has an active Develop Turn: ${activeDevelopTurn.id}.`);
2727
2202
  }
2728
- if (item.assignee !== undefined && store.getActiveAgentRun(task.id, item.assignee) !== null) {
2729
- throw new Error(`Role has an active Run: ${task.id}/${item.assignee}.`);
2203
+ if (item.assignee !== undefined && store.getActiveTurn(task.id, item.assignee) !== null) {
2204
+ throw new Error(`Role has an active Turn: ${task.id}/${item.assignee}.`);
2730
2205
  }
2731
2206
  }
2732
2207
  function assertWorkItemOwnsWorkspace(item, workspace) {
@@ -2746,16 +2221,16 @@ function preserveWorkspaceCreatedAt(workspace, previous) {
2746
2221
  : { ...workspace, createdAt: previous.createdAt };
2747
2222
  }
2748
2223
  function assertWorkspaceSessionsRetirable(store, taskId, roleName, now) {
2749
- if (store.getActiveAgentRun(taskId, roleName) !== null) {
2750
- throw new Error(`Role has an active Run: ${taskId}/${roleName}.`);
2224
+ if (store.getActiveTurn(taskId, roleName) !== null) {
2225
+ throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
2751
2226
  }
2752
2227
  const sessions = store.getTaskRoleSessionSet(taskId, roleName);
2753
2228
  if (sessions !== null)
2754
2229
  retireTaskRoleSessionsForWorkspace(sessions, now);
2755
2230
  }
2756
2231
  function retireWorkspaceBoundSession(store, taskId, roleName, now) {
2757
- if (store.getActiveAgentRun(taskId, roleName) !== null) {
2758
- throw new Error(`Role has an active Run: ${taskId}/${roleName}.`);
2232
+ if (store.getActiveTurn(taskId, roleName) !== null) {
2233
+ throw new Error(`Role has an active Turn: ${taskId}/${roleName}.`);
2759
2234
  }
2760
2235
  const sessions = store.getTaskRoleSessionSet(taskId, roleName);
2761
2236
  if (sessions !== null) {
@@ -2777,7 +2252,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2777
2252
  .sort();
2778
2253
  if (!isDeepStrictEqual(writableProjects, [...item.writeProjectIds].sort()))
2779
2254
  return false;
2780
- const run = store.getActiveAgentRun(taskId, role.name);
2255
+ const run = store.getActiveTurn(taskId, role.name);
2781
2256
  if (run === null
2782
2257
  || run.status !== "active"
2783
2258
  || run.purpose !== "execution"
@@ -2794,10 +2269,6 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2794
2269
  || sessions.owner.taskId !== taskId
2795
2270
  || sessions.owner.roleName !== role.name
2796
2271
  || sessions.activeAgentId !== role.activeAgentId
2797
- || sessions.inFlight === null
2798
- || sessions.inFlight.agentId !== role.activeAgentId
2799
- || sessions.inFlight.runId !== run.id
2800
- || sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)
2801
2272
  || session === undefined
2802
2273
  || session.agentId !== role.activeAgentId
2803
2274
  || session.adapterId !== run.effective.adapterId
@@ -2820,7 +2291,7 @@ function canCorrectActiveWorkItemRoleWorkspaceHint(store, taskId, role, item, wo
2820
2291
  && isRuntimeLaunchReservation(lifecycle)) {
2821
2292
  const executionRef = lifecycle.executionRef;
2822
2293
  if (!isRuntimeLaunchReservation(lifecycle, session.launchId)
2823
- || executionRef?.type !== "run"
2294
+ || executionRef?.type !== "turn"
2824
2295
  || executionRef.taskId !== taskId
2825
2296
  || executionRef.id !== run.id)
2826
2297
  return false;
@@ -2838,34 +2309,6 @@ function isMissingPath(error) {
2838
2309
  return typeof error === "object" && error !== null && "code" in error
2839
2310
  && error.code === "ENOENT";
2840
2311
  }
2841
- /**
2842
- * The token-bearing ref segments (`task-N-<8hex>`) under a Project's worktree
2843
- * container for one Task. A missing container yields no segments; legacy
2844
- * (`task-N`) and foreign-Task directories are ignored.
2845
- */
2846
- async function listTaskTokenSegments(container, taskId) {
2847
- let entries;
2848
- try {
2849
- entries = await readdir(container, { withFileTypes: true });
2850
- }
2851
- catch (error) {
2852
- if (isMissingPath(error))
2853
- return [];
2854
- throw error;
2855
- }
2856
- const prefix = `${taskId}-`;
2857
- const segments = [];
2858
- for (const entry of entries) {
2859
- if (!entry.isDirectory())
2860
- continue;
2861
- if (!entry.name.startsWith(prefix))
2862
- continue;
2863
- if (!TASK_WORKSPACE_TOKEN_PATTERN.test(entry.name.slice(prefix.length)))
2864
- continue;
2865
- segments.push(entry.name);
2866
- }
2867
- return segments.sort();
2868
- }
2869
2312
  function recordWorkspaceDisposition(store, taskId, workItem, now) {
2870
2313
  const item = store.getWorkItem(taskId, workItem.workItemId);
2871
2314
  if (item === null)