@zq-silk/yui 0.15.11 → 0.16.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 (280) hide show
  1. package/ARCHITECTURE.md +11 -6
  2. package/ARCHITECTURE.zh-CN.md +8 -4
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/artifacts/artifactCommitLock.js +16 -55
  6. package/dist/artifacts/taskArtifactRepository.js +3 -3
  7. package/dist/cli/agentConfigurationPicker.js +1 -1
  8. package/dist/cli/commandCatalog.js +92 -48
  9. package/dist/cli/completion.js +18 -18
  10. package/dist/cli/completionWizard.js +1 -1
  11. package/dist/cli/dynamicCompletion.js +1 -1
  12. package/dist/cli/interactionPolicy.js +11 -3
  13. package/dist/cli/invocationAuthority.js +64 -0
  14. package/dist/cli/managedDiagnostics.js +2 -2
  15. package/dist/cli/parseRepeatable.js +35 -0
  16. package/dist/cli/updatePorts.js +26 -5
  17. package/dist/cli.js +1303 -1235
  18. package/dist/commands/agentCommands.js +4 -4
  19. package/dist/commands/capabilityCommands.js +1 -1
  20. package/dist/commands/configCommands.js +6 -57
  21. package/dist/commands/configOverview.js +2 -2
  22. package/dist/commands/durableJobCommands.js +12 -6
  23. package/dist/commands/executionAuditCommands.js +10 -0
  24. package/dist/commands/globalRoleCommands.js +78 -29
  25. package/dist/commands/grantCommands.js +0 -3
  26. package/dist/commands/jobCommands.js +1 -4
  27. package/dist/commands/operatorCommands.js +3 -3
  28. package/dist/commands/projectCommands.js +45 -25
  29. package/dist/commands/resourcesCommands.js +10 -40
  30. package/dist/commands/roleConfiguration.js +2 -6
  31. package/dist/commands/taskActivationCommands.js +4 -8
  32. package/dist/commands/taskAgentCapabilities.js +9 -0
  33. package/dist/commands/taskCommandSupport.js +155 -0
  34. package/dist/commands/taskCommandTypes.js +1 -0
  35. package/dist/commands/taskCommands.js +355 -732
  36. package/dist/commands/taskCompletionGate.js +1 -1
  37. package/dist/commands/taskExecutionCommands.js +1 -1
  38. package/dist/commands/taskFactCommands.js +325 -0
  39. package/dist/commands/taskInputCommands.js +12 -42
  40. package/dist/commands/taskIntegrationCommands.js +20 -58
  41. package/dist/commands/taskNextActionCommand.js +7 -6
  42. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  43. package/dist/commands/taskPublicationCommands.js +12 -3
  44. package/dist/commands/taskPublicationVerifyCommand.js +24 -40
  45. package/dist/commands/taskRemoteDeliveryCommand.js +5 -86
  46. package/dist/commands/taskUpstreamCommands.js +19 -8
  47. package/dist/commands/workflowCommands.js +6 -1
  48. package/dist/completion/completionInstaller.js +26 -26
  49. package/dist/completion/completionState.js +26 -26
  50. package/dist/completion/fileCompletionManager.js +6 -11
  51. package/dist/config/configCatalog.js +0 -2
  52. package/dist/config/timeZone.js +14 -0
  53. package/dist/config/yuiConfig.js +0 -29
  54. package/dist/context/runContextPack.js +5 -3
  55. package/dist/context/taskCatalog.js +185 -0
  56. package/dist/context/taskContext.js +152 -151
  57. package/dist/context/wakeRunReferences.js +11 -0
  58. package/dist/controller/agentCapabilities.js +58 -0
  59. package/dist/controller/agentRuntimeObserver.js +6 -11
  60. package/dist/controller/clientRuntime.js +22 -14
  61. package/dist/controller/controller.js +20 -24
  62. package/dist/controller/fileSchedulerStoreAdapter.js +191 -225
  63. package/dist/controller/globalInputDelivery.js +13 -0
  64. package/dist/controller/jobClient.js +4 -4
  65. package/dist/controller/jobControl.js +60 -25
  66. package/dist/controller/jobSupervisor.js +4 -4
  67. package/dist/controller/providerRetryAdmission.js +100 -0
  68. package/dist/controller/providerRetryDelivery.js +218 -0
  69. package/dist/controller/runtime.js +85 -43
  70. package/dist/controller/runtimeEventProcessor.js +0 -5
  71. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  72. package/dist/controller/taskAgentError.js +35 -0
  73. package/dist/coordination/workMailboxQueue.js +2 -24
  74. package/dist/core/controllerClient.js +2 -2
  75. package/dist/core/fileLockOwner.js +74 -0
  76. package/dist/decision/decision.js +17 -0
  77. package/dist/doctor/doctor.js +18 -9
  78. package/dist/event/taskEvent.js +12 -0
  79. package/dist/execution/workItemExecutionProjection.js +2 -2
  80. package/dist/executor/agentCapabilityConfig.js +43 -0
  81. package/dist/executor/agentConfigurationCatalog.js +49 -11
  82. package/dist/executor/agentExecutor.js +5 -8
  83. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  84. package/dist/executor/taskAgentCapabilities.js +64 -0
  85. package/dist/integration/deliveryObligation.js +1 -69
  86. package/dist/integration/gitIntegrationService.js +79 -90
  87. package/dist/integration/integrationAttempt.js +3 -14
  88. package/dist/integration/integrationSourceApplication.js +4 -12
  89. package/dist/integration/manifestTags.js +2 -2
  90. package/dist/job/durableJob.js +4 -25
  91. package/dist/job/jobAssignmentScope.js +22 -0
  92. package/dist/job/jobOperation.js +16 -0
  93. package/dist/job/jobRunner.js +2 -1
  94. package/dist/job/stepDirectory.js +15 -0
  95. package/dist/kernel/builtinCapabilities.js +15 -11
  96. package/dist/kernel/kernelPorts.js +1 -17
  97. package/dist/lifecycle/exactRunTerminalization.js +5 -10
  98. package/dist/message/globalProviderRetry.js +15 -0
  99. package/dist/message/inputControlResolution.js +3 -4
  100. package/dist/message/message.js +17 -20
  101. package/dist/message/messageContinuation.js +1 -1
  102. package/dist/milestone/milestone.js +11 -0
  103. package/dist/observability/executionAudit.js +19 -0
  104. package/dist/observability/orchestrationMetrics.js +24 -1
  105. package/dist/observability/runtimeIdentity.js +1 -17
  106. package/dist/output/agentConfigurationPresentation.js +2 -0
  107. package/dist/output/timePresentation.js +1 -14
  108. package/dist/plugins/pluginService.js +12 -4
  109. package/dist/release/releaseWorkflowPorts.js +45 -96
  110. package/dist/release/runtimeRelease.js +9 -15
  111. package/dist/repository/gitWorkspace.js +371 -105
  112. package/dist/repository/projectMaintenanceLock.js +98 -82
  113. package/dist/repository/taskBaseFreshness.js +1 -1
  114. package/dist/repository/taskWorkspaceCoordinator.js +82 -144
  115. package/dist/repository/taskWorkspacePreparer.js +113 -47
  116. package/dist/repository/workspaceCleanupInspection.js +187 -0
  117. package/dist/resources/autoResourceGc.js +4 -77
  118. package/dist/resources/liveReferences.js +7 -54
  119. package/dist/resources/resourceDiscovery.js +5 -118
  120. package/dist/resources/resourceGc.js +233 -181
  121. package/dist/resources/resourceRegistrar.js +5 -4
  122. package/dist/resources/resourceRegistry.js +3 -37
  123. package/dist/resources/resourceRegistryStore.js +13 -36
  124. package/dist/resources/sqliteResourceRegistry.js +43 -26
  125. package/dist/review/deltaRecheck.js +1 -1
  126. package/dist/review/reviewAcceptance.js +1 -1
  127. package/dist/review/reviewDecision.js +1 -1
  128. package/dist/review/reviewRound.js +8 -7
  129. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  130. package/dist/runtime/acpProtocol.js +4 -51
  131. package/dist/runtime/acpSession.js +10 -75
  132. package/dist/runtime/acpSessionConfiguration.js +1 -8
  133. package/dist/runtime/agentEndpoint.js +3 -2
  134. package/dist/runtime/agentError.js +5 -3
  135. package/dist/runtime/agentFailureContext.js +43 -0
  136. package/dist/runtime/agentHost.js +42 -25
  137. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  138. package/dist/runtime/codexAppServerRuntime.js +34 -3
  139. package/dist/runtime/codexInteractiveHost.js +2 -2
  140. package/dist/runtime/index.js +0 -1
  141. package/dist/runtime/managedCaller.js +21 -1
  142. package/dist/runtime/providerControl.js +5 -1
  143. package/dist/runtime/providerErrors.js +38 -0
  144. package/dist/runtime/providerRetry.js +198 -0
  145. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  146. package/dist/runtime/runtimeObservation.js +0 -5
  147. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  148. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  149. package/dist/runtime/sessionTokenMetrics.js +15 -5
  150. package/dist/runtime/structuredProviderHost.js +10 -43
  151. package/dist/runtime/taskUsageMetrics.js +275 -0
  152. package/dist/runtime/tmuxAdapters.js +6 -8
  153. package/dist/scheduler/activeRoleRunDelivery.js +19 -7
  154. package/dist/scheduler/leaderWakeupProcessor.js +24 -6
  155. package/dist/scheduler/operatorEvent.js +12 -20
  156. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  157. package/dist/scheduler/ports.js +5 -8
  158. package/dist/scheduler/roleRunLiveness.js +4 -4
  159. package/dist/scheduler/roleRunStall.js +16 -23
  160. package/dist/scheduler/taskExecutionProjection.js +49 -36
  161. package/dist/scheduler/taskObservabilityProjection.js +6 -45
  162. package/dist/scheduler/taskWake.js +5 -10
  163. package/dist/scheduler/wakeReason.js +2 -0
  164. package/dist/scheduler/wakeupQueue.js +2 -4
  165. package/dist/setup/setupCommand.js +1 -1
  166. package/dist/storage/contextRecords.js +106 -0
  167. package/dist/storage/currentTaskStore.js +1 -1
  168. package/dist/storage/homeLayout.js +13 -17
  169. package/dist/storage/migrations/agentFailureContext.js +22 -0
  170. package/dist/storage/migrations/currentInputContract.js +86 -0
  171. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  172. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  173. package/dist/storage/migrations/integrationContinuation.js +5 -4
  174. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  175. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  176. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  177. package/dist/storage/migrations/verificationPolicy.js +74 -0
  178. package/dist/storage/migrations/workItemHistory.js +46 -0
  179. package/dist/storage/persistenceWorker.js +12 -4
  180. package/dist/storage/recordValidation.js +87 -0
  181. package/dist/storage/sqliteSchema.js +173 -1
  182. package/dist/storage/sqliteStore.js +172 -161
  183. package/dist/storage/storageSchema.js +2 -14
  184. package/dist/storage/storageVersions.js +1 -1
  185. package/dist/storage/storeRpc.js +16 -7
  186. package/dist/storage/taskCatalog.js +123 -0
  187. package/dist/storage/taskStore.js +5 -7
  188. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  189. package/dist/task/archiveDiagnostics.js +1 -0
  190. package/dist/task/archivePreflight.js +124 -0
  191. package/dist/task/completionReadiness.js +5 -24
  192. package/dist/task/draftPlan.js +0 -53
  193. package/dist/task/nextAction.js +7 -13
  194. package/dist/task/publicationAdoption.js +56 -0
  195. package/dist/task/publicationReference.js +10 -0
  196. package/dist/task/remoteDelivery.js +31 -16
  197. package/dist/task/remoteDeliveryService.js +89 -0
  198. package/dist/task/taskActivation.js +2 -25
  199. package/dist/task/taskActivationService.js +0 -2
  200. package/dist/task/taskRecordReference.js +0 -1
  201. package/dist/task/taskSubmission.js +3 -14
  202. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  203. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  204. package/dist/telemetry/telemetryWiring.js +4 -3
  205. package/dist/tmux/tmuxManager.js +29 -20
  206. package/dist/verification/gateArtifact.js +15 -24
  207. package/dist/verification/gateArtifactStore.js +14 -7
  208. package/dist/verification/verificationGateService.js +29 -88
  209. package/dist/verification/verificationPlan.js +27 -72
  210. package/dist/web/assets/client/app.js +92 -17
  211. package/dist/web/assets/client/components.js +55 -13
  212. package/dist/web/assets/client/i18n.js +72 -4
  213. package/dist/web/assets/client/taskSurface.js +6 -5
  214. package/dist/web/assets/client/view.js +35 -7
  215. package/dist/web/assets/shell.js +6 -0
  216. package/dist/web/assets/styles/layout.js +7 -0
  217. package/dist/web/webServer.js +12 -3
  218. package/dist/web/webSnapshot.js +14 -85
  219. package/dist/web/webTaskSurface.js +17 -46
  220. package/dist/workItem/workItem.js +1 -12
  221. package/dist/workspace/cleanupInspection.js +63 -0
  222. package/dist/workspace/workItemChangeSetManager.js +110 -50
  223. package/docs/agent-result-consumption.md +4 -0
  224. package/docs/agent-result-consumption.zh-CN.md +3 -0
  225. package/docs/agent-runtime-drivers.md +7 -0
  226. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  227. package/docs/architecture/README.md +2 -0
  228. package/docs/architecture/README.zh-CN.md +3 -1
  229. package/docs/architecture/capabilities-and-resources.md +42 -5
  230. package/docs/architecture/capabilities-and-resources.zh-CN.md +33 -3
  231. package/docs/managed-turn-and-session-runtime.md +121 -1
  232. package/docs/managed-turn-and-session-runtime.zh-CN.md +96 -2
  233. package/docs/observability/README.md +62 -0
  234. package/docs/observability/README.zh-CN.md +47 -0
  235. package/docs/plugin-sdk.md +4 -2
  236. package/docs/project-refresh.md +77 -0
  237. package/docs/project-refresh.zh-CN.md +59 -0
  238. package/docs/provider-retry.md +70 -0
  239. package/docs/provider-runtime.md +3 -1
  240. package/docs/provider-runtime.zh-CN.md +4 -2
  241. package/docs/release-workflow.md +190 -7
  242. package/docs/release-workflow.zh-CN.md +139 -5
  243. package/docs/roles-and-configuration.md +29 -0
  244. package/docs/roles-and-configuration.zh-CN.md +21 -0
  245. package/docs/sqlite-control-plane-design.md +16 -3
  246. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  247. package/docs/task-dag-semantics.md +3 -3
  248. package/docs/task-dag-semantics.zh-CN.md +2 -2
  249. package/docs/task-delivery.md +223 -17
  250. package/docs/task-delivery.zh-CN.md +171 -14
  251. package/docs/task-discovery.md +101 -0
  252. package/docs/task-discovery.zh-CN.md +85 -0
  253. package/docs/testing/verification-levels.md +161 -9
  254. package/docs/testing/verification-levels.zh-CN.md +116 -9
  255. package/i18n/README.zh-CN.md +13 -7
  256. package/package.json +1 -1
  257. package/skills/yui-leader/SKILL.md +5 -0
  258. package/skills/yui-leader/references/execution.md +3 -2
  259. package/skills/yui-leader/references/integration.md +9 -5
  260. package/skills/yui-leader/references/planning.md +9 -3
  261. package/skills/yui-operator/SKILL.md +24 -8
  262. package/skills/yui-reviewer/SKILL.md +4 -0
  263. package/skills/yui-runtime/SKILL.md +17 -2
  264. package/skills/yui-runtime/references/publication.md +26 -4
  265. package/skills/yui-runtime/references/recovery.md +71 -0
  266. package/dist/agent/agentRegistry.js +0 -10
  267. package/dist/agentRun/runIdentity.js +0 -22
  268. package/dist/commands/deliveryGuardPreflight.js +0 -30
  269. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  270. package/dist/commands/taskOverviewCommand.js +0 -376
  271. package/dist/completion/completionWizard.js +0 -125
  272. package/dist/context/dispatchContext.js +0 -110
  273. package/dist/context/wakeNotification.js +0 -144
  274. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  275. package/dist/integration/integrationQueueEntry.js +0 -191
  276. package/dist/integration/integrationQueueService.js +0 -810
  277. package/dist/release/fakeReleasePorts.js +0 -55
  278. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  279. package/dist/task/deliveryGuard.js +0 -226
  280. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
@@ -1,29 +1,31 @@
1
- import { lstat, mkdir, readlink, readdir, rm, rmdir, symlink, unlink } from "node:fs/promises";
1
+ import { lstat, mkdir, readdir, readlink, rm, rmdir, symlink, unlink } from "node:fs/promises";
2
2
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
3
3
  import { isDeepStrictEqual } from "node:util";
4
4
  import { recordArchiveCleanup } from "../task/archiveDiagnostics.js";
5
+ import { enqueueWork } from "../coordination/workMailboxQueue.js";
6
+ import { createTaskEvent } from "../event/taskEvent.js";
5
7
  import { retireTaskRoleSessionsForWorkspace, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
6
8
  import { formatWorkspacePreflightError } from "../executor/workspacePreflightClassification.js";
9
+ import { createProjectResources } from "../resources/projectResourceService.js";
10
+ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
11
+ import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
7
12
  import { updateRole } from "../role/role.js";
13
+ import { hasRuntimeCleanupObligation, RUNTIME_HOST_DETACH_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
8
14
  import { taskRoleRuntimeIdentity } from "../runtime/managedCaller.js";
9
- import { hasRuntimeCleanupObligation, runtimeLifecycleTarget, RUNTIME_HOST_DETACH_REQUIRED_REASON } from "../runtime/lifecycleReservation.js";
10
- import { taskLocalActor } from "../commands/taskActor.js";
11
- import { attachReviewRoundWorkspace, recordReviewWorkspaceDisposition } from "../review/reviewRound.js";
15
+ import { managedTaskRoot } from "../storage/homeLayout.js";
12
16
  import { StorageConflictError } from "../storage/taskStore.js";
13
- import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
14
- import { admitStoredTaskActivation, adoptTaskActivationResources, recordFailedTaskActivation, recordAdoptedTaskActivation } from "../task/taskActivationService.js";
15
17
  import { validateDraftTaskForActivation } from "../task/draftPlan.js";
16
- import { createProjectResources } from "../resources/projectResourceService.js";
17
- import { createTaskEvent } from "../event/taskEvent.js";
18
- import { enqueueWork } from "../coordination/workMailboxQueue.js";
19
- import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, workItemExecutionGroupById, recordWorkItemWorkspaceDisposition } from "../workItem/workItem.js";
18
+ import { activateTask, bindTaskProjectCommits, bindTaskWorkspaceIdentity, synchronizeTaskProjectCommits, taskOwnsManagedWorkspace } from "../task/task.js";
19
+ import { admitStoredTaskActivation, adoptTaskActivationResources, recordAdoptedTaskActivation, recordFailedTaskActivation } from "../task/taskActivationService.js";
20
+ import { taskLocalActor } from "../task/taskAuthority.js";
21
+ import { createCandidateGitSnapshot, createDirectTaskMainSnapshot, recordWorkItemWorkspaceDisposition, workItemExecutionGroupById } from "../workItem/workItem.js";
22
+ import { CleanupInspectionError } from "../workspace/cleanupInspection.js";
20
23
  import { createManagedWorkspace, isTaskOwnedWorkspace, managedWorkspaceKey, managedWorktreeName, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
21
24
  import { NodeGitWorkspace, worktreeIdentity } from "./gitWorkspace.js";
22
- import { acquireProjectMaintenanceLocks } from "./projectMaintenanceLock.js";
23
- import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
24
- import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
25
+ import { acquireProjectMaintenanceLocks, ProjectMaintenanceLockCancelledError, ProjectMaintenanceLockedError } from "./projectMaintenanceLock.js";
25
26
  import { captureTaskBaseProvenance, recordTaskBaseProvenanceEvents } from "./taskBaseFreshness.js";
26
- import { managedTaskRoot } from "../storage/homeLayout.js";
27
+ import { generateTaskWorkspaceIdentity, taskWorkspaceRefSegment, validateTaskWorkspaceIdentity } from "./taskWorkspaceIdentity.js";
28
+ import { inspectWorkspaceCleanup } from "./workspaceCleanupInspection.js";
27
29
  const MAIN_WORKTREE = "main";
28
30
  const LEADER_ROLE = "leader";
29
31
  /**
@@ -62,11 +64,21 @@ export class FileTaskWorkspacePreparer {
62
64
  store;
63
65
  git;
64
66
  now;
65
- constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date()) {
67
+ maintenanceLockOptions;
68
+ constructor(home, store, git = new NodeGitWorkspace(), now = () => new Date(), maintenanceLockOptions = {}) {
66
69
  this.home = home;
67
70
  this.store = store;
68
71
  this.git = git;
69
72
  this.now = now;
73
+ this.maintenanceLockOptions = maintenanceLockOptions;
74
+ }
75
+ inspectWorkspaceCleanup(workspace, disposition, forceArchive = false) {
76
+ return inspectWorkspaceCleanup(this.store, this.git, workspace, disposition, forceArchive);
77
+ }
78
+ async #assertWorkspaceCleanup(workspace, disposition) {
79
+ const checks = await this.inspectWorkspaceCleanup(workspace, disposition);
80
+ if (checks.length > 0)
81
+ throw new CleanupInspectionError(checks);
70
82
  }
71
83
  #resourceRegistrarValue;
72
84
  #resourceRegistrar() {
@@ -75,7 +87,7 @@ export class FileTaskWorkspacePreparer {
75
87
  #registerWorkspace(workspace) {
76
88
  this.#resourceRegistrar().registerManagedWorkspace(workspace);
77
89
  }
78
- async prepareTaskWorkspace(taskId) {
90
+ async prepareTaskWorkspace(taskId, signal) {
79
91
  // The per-Project maintenance fence makes prepare mutually exclusive with
80
92
  // Project maintenance and Task archive cleanup: a concurrent migration must not switch the
81
93
  // Project catalog to the Home-managed repo while prepare is creating
@@ -87,7 +99,7 @@ export class FileTaskWorkspacePreparer {
87
99
  for (let attempt = 0;; attempt += 1) {
88
100
  try {
89
101
  const task = requireTask(this.store, taskId);
90
- const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
102
+ const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
91
103
  try {
92
104
  return await this.#prepareTaskWorkspaceLocked(current.id, false);
93
105
  }
@@ -116,11 +128,14 @@ export class FileTaskWorkspacePreparer {
116
128
  * later launch failure can never present itself as "activation never
117
129
  * happened".
118
130
  */
119
- async activateTaskWorkspace(taskId, environment = {}) {
131
+ async activateTaskWorkspace(taskId, environment = {}, signal) {
120
132
  for (let attempt = 0;; attempt += 1) {
121
133
  let attemptedRequest;
122
134
  try {
123
135
  const task = requireTask(this.store, taskId);
136
+ if (signal?.aborted) {
137
+ throw new ProjectMaintenanceLockCancelledError(task.projectBindings[0]?.projectId ?? task.id, signal.reason);
138
+ }
124
139
  const caller = taskLocalActor(this.store, environment, task.id);
125
140
  const actor = task.activationRequest?.operation.actorId === `task:${task.id}/role:leader`
126
141
  ? "leader" : caller;
@@ -139,21 +154,27 @@ export class FileTaskWorkspacePreparer {
139
154
  changed: false
140
155
  };
141
156
  }
157
+ const admission = admitStoredTaskActivation(this.store, task.id);
158
+ if (admission.disposition === "absent") {
159
+ throw new Error(`Task activation request is required: ${task.id}. `
160
+ + `Use yui task activation request ${task.id} --request-id <id> --environment <plan> first.`);
161
+ }
142
162
  const provider = this.store.getTaskRoleSessionSet(task.id, LEADER_ROLE)?.providerBinding;
143
163
  if (provider?.run != null
144
164
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
145
165
  throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
146
166
  }
147
- const admission = admitStoredTaskActivation(this.store, task.id);
148
167
  if (admission.disposition === "ready")
149
168
  attemptedRequest = admission.request;
150
169
  validateDraftTaskForActivation(this.store, task);
151
- const adopted = await this.#adoptActivationEnvironment(task);
152
- return adopted.workspaceFree
153
- ? this.#activateWorkspaceFreeTask(task.id, adopted, actor)
154
- : await this.#prepareActivatedTaskWorkspace(task, adopted, actor);
170
+ return await this.#prepareActivatedTaskWorkspace(task, actor, environment, signal);
155
171
  }
156
172
  catch (error) {
173
+ // Waiting did not execute the workspace operation. Preserve the
174
+ // activation request instead of turning contention/stop into failure.
175
+ if (error instanceof ProjectMaintenanceLockedError
176
+ || error instanceof ProjectMaintenanceLockCancelledError)
177
+ throw error;
157
178
  if (error instanceof StorageConflictError
158
179
  && attempt < TASK_WORKSPACE_PREPARE_MAX_CONFLICT_RETRIES) {
159
180
  continue;
@@ -170,10 +191,28 @@ export class FileTaskWorkspacePreparer {
170
191
  }
171
192
  }
172
193
  /** Prepares the managed workspace of a Task being activated, under its fences. */
173
- async #prepareActivatedTaskWorkspace(task, environment, actor) {
174
- const { release, current } = this.#acquireTaskProjectMaintenanceLocks(task);
194
+ async #prepareActivatedTaskWorkspace(task, actor, callerEnvironment, signal) {
195
+ const { release, current } = await this.#acquireTaskProjectMaintenanceLocks(task, signal);
175
196
  try {
176
- return await this.#prepareTaskWorkspaceLocked(current.id, true, environment, actor);
197
+ taskLocalActor(this.store, callerEnvironment, current.id);
198
+ if (current.executionGate.state !== "enabled") {
199
+ throw new Error(`Task execution is not enabled: ${current.id}.`);
200
+ }
201
+ if (!isDeepStrictEqual(current.activationRequest, task.activationRequest)) {
202
+ throw new Error(`Task activation request changed while waiting for Project maintenance: ${task.id}.`);
203
+ }
204
+ const provider = this.store.getTaskRoleSessionSet(current.id, LEADER_ROLE)?.providerBinding;
205
+ if (provider?.run != null
206
+ && ["submitting", "accepted", "delivery-unknown"].includes(provider.run.status)) {
207
+ throw new Error("Planning native input is unsettled; preserve activation intent and retry after its exact terminal.");
208
+ }
209
+ validateDraftTaskForActivation(this.store, current);
210
+ // Acquire before resource adoption: a timed-out/cancelled waiter must
211
+ // not leave an adopted environment to be repeated on the next attempt.
212
+ const adopted = await this.#adoptActivationEnvironment(current);
213
+ return adopted.workspaceFree
214
+ ? this.#activateWorkspaceFreeTask(current.id, adopted, actor)
215
+ : await this.#prepareTaskWorkspaceLocked(current.id, true, adopted, actor);
177
216
  }
178
217
  finally {
179
218
  release();
@@ -183,9 +222,8 @@ export class FileTaskWorkspacePreparer {
183
222
  * Prepares and adopts the resource configuration an explicit activation
184
223
  * request named, before any status change.
185
224
  *
186
- * A Task with no request keeps the historical behavior: its managed workspace
187
- * is the whole environment. A request whose plan is empty and whose Task
188
- * binds no Project owns nothing physical, and is reported as workspace-free
225
+ * A request whose plan is empty and whose Task binds no Project owns nothing
226
+ * physical, and is reported as workspace-free
189
227
  * so activation creates no directory at all.
190
228
  *
191
229
  * The exact request that was adopted travels with the result, so the later
@@ -194,8 +232,6 @@ export class FileTaskWorkspacePreparer {
194
232
  */
195
233
  async #adoptActivationEnvironment(task) {
196
234
  const admission = admitStoredTaskActivation(this.store, task.id);
197
- if (admission.disposition === "absent")
198
- return { workspaceFree: false };
199
235
  if (admission.disposition !== "ready") {
200
236
  throw new Error(`Task activation is not adoptable: ${task.id}/${admission.disposition}`
201
237
  + (admission.disposition === "unavailable" ? ` (${admission.reason})` : "")
@@ -330,12 +366,19 @@ export class FileTaskWorkspacePreparer {
330
366
  * caller re-reads the Task and re-acquires the correct fence set. The
331
367
  * caller owns `release` and MUST call it on every exit path.
332
368
  */
333
- #acquireTaskProjectMaintenanceLocks(task) {
369
+ async #acquireTaskProjectMaintenanceLocks(task, signal) {
334
370
  const projectIds = task.projectBindings.map(({ projectId }) => projectId);
371
+ const waitSignal = signal ?? this.maintenanceLockOptions.signal;
335
372
  const release = projectIds.length === 0
336
373
  ? () => { }
337
- : acquireProjectMaintenanceLocks(this.home, projectIds);
374
+ : await acquireProjectMaintenanceLocks(this.home, projectIds, {
375
+ ...this.maintenanceLockOptions,
376
+ signal: waitSignal
377
+ });
338
378
  try {
379
+ if (waitSignal?.aborted) {
380
+ throw new ProjectMaintenanceLockCancelledError(projectIds[0] ?? task.id, waitSignal.reason);
381
+ }
339
382
  const current = requireTask(this.store, task.id);
340
383
  const currentIds = current.projectBindings.map(({ projectId }) => projectId).sort();
341
384
  const lockedIds = [...projectIds].sort();
@@ -358,13 +401,16 @@ export class FileTaskWorkspacePreparer {
358
401
  * catalog in that gap. The caller owns `release` and must call it on every
359
402
  * exit path.
360
403
  */
361
- acquireTaskProjectMaintenanceLocks(taskId) {
362
- return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId));
404
+ acquireTaskProjectMaintenanceLocks(taskId, signal) {
405
+ return this.#acquireTaskProjectMaintenanceLocks(requireTask(this.store, taskId), signal);
363
406
  }
364
407
  async #prepareTaskWorkspaceLocked(taskId, activate,
365
408
  /** Environment and request identity the activation adopted, when one exists. */
366
409
  activationEnvironment = { workspaceFree: false }, actor = "user") {
367
410
  const task = requireTask(this.store, taskId);
411
+ if (task.executionGate.state !== "enabled") {
412
+ throw new Error(`Task execution is not enabled: ${task.id}.`);
413
+ }
368
414
  if (activate && task.status !== "draft") {
369
415
  throw new Error(`Only a Draft Task can atomically adopt a workspace: ${task.id}/${task.status}.`);
370
416
  }
@@ -910,15 +956,17 @@ export class FileTaskWorkspacePreparer {
910
956
  return projects;
911
957
  }
912
958
  async prepareWorkItemWorkspace(taskId, workItemId) {
913
- const item = requireWorkItem(this.store, taskId, workItemId);
959
+ let item = requireWorkItem(this.store, taskId, workItemId);
914
960
  const task = requireTask(this.store, item.taskId);
915
961
  assertWorkItemWorkspaceEligible(this.store, task, item);
916
962
  // Hold the per-Project maintenance fence across the ensure-main-prepared
917
963
  // and the WorkItem worktree creation, so concurrent Project maintenance
918
964
  // cannot switch the Project catalog between the two. The under-lock Task
919
965
  // snapshot drives every Project read and Git effect below.
920
- const { release, current: lockedTask } = this.#acquireTaskProjectMaintenanceLocks(task);
966
+ const { release, current: lockedTask } = await this.#acquireTaskProjectMaintenanceLocks(task);
921
967
  try {
968
+ item = requireWorkItem(this.store, taskId, workItemId);
969
+ assertWorkItemWorkspaceEligible(this.store, lockedTask, item);
922
970
  const lockedProjectIds = lockedTask.projectBindings.map(({ projectId }) => projectId);
923
971
  await this.#prepareTaskWorkspaceWithRetries(lockedTask.id, lockedProjectIds);
924
972
  // prepareTaskWorkspace may have just minted and persisted the workspace
@@ -1099,9 +1147,12 @@ export class FileTaskWorkspacePreparer {
1099
1147
  // already holds (one locked boundary across preparation and adoption);
1100
1148
  // otherwise the fence is acquired here.
1101
1149
  const { release, current: lockedTask } = heldFence === undefined
1102
- ? this.#acquireTaskProjectMaintenanceLocks(task)
1150
+ ? await this.#acquireTaskProjectMaintenanceLocks(task)
1103
1151
  : { release: () => { }, current: heldFence.current };
1104
1152
  try {
1153
+ if (lockedTask.status !== "active" || lockedTask.executionGate.state !== "enabled") {
1154
+ throw new Error(`Task is not open for Execution Lane preparation: ${lockedTask.id}.`);
1155
+ }
1105
1156
  const lineage = executionLaneLineage(this.store, lockedTask, executionGroupId, executionLaneId, hint);
1106
1157
  const item = lineage.purpose === "execution"
1107
1158
  ? this.store.getWorkItem(taskId, lineage.workItemId)
@@ -1269,6 +1320,7 @@ export class FileTaskWorkspacePreparer {
1269
1320
  && owner.executionLaneId === executionLaneId));
1270
1321
  if (workspace === undefined)
1271
1322
  return "missing";
1323
+ await this.#assertWorkspaceCleanup(workspace, "abandoned");
1272
1324
  const state = await this.#inspectEntries(task.id, this.#taskSegment(task), managedWorktreeName(workspace.owner), workspace.entries.filter(({ access }) => access === "write"));
1273
1325
  if (state === "dirty")
1274
1326
  return "dirty";
@@ -1404,8 +1456,11 @@ export class FileTaskWorkspacePreparer {
1404
1456
  * never used as a ChangeSet capture source. */
1405
1457
  async prepareReviewRoundWorkspace(taskId, reviewRoundId) {
1406
1458
  const taskSnapshot = requireTask(this.store, taskId);
1407
- const { release, current: task } = this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
1459
+ const { release, current: task } = await this.#acquireTaskProjectMaintenanceLocks(taskSnapshot);
1408
1460
  try {
1461
+ if (task.status !== "active" || task.executionGate.state !== "enabled") {
1462
+ throw new Error(`Task is not open for ReviewRound preparation: ${task.id}.`);
1463
+ }
1409
1464
  const taskSegment = this.#taskSegment(task);
1410
1465
  const round = this.store.getReviewRound(task.id, reviewRoundId);
1411
1466
  if (round === null)
@@ -1413,7 +1468,7 @@ export class FileTaskWorkspacePreparer {
1413
1468
  if (round.status !== "pending") {
1414
1469
  throw new Error(`ReviewRound workspace can only prepare while pending: ${round.id}.`);
1415
1470
  }
1416
- const taskScope = (round.scope ?? "work-item") === "task";
1471
+ const taskScope = round.scope === "task";
1417
1472
  const item = taskScope || round.workItemId === undefined
1418
1473
  ? undefined
1419
1474
  : requireWorkItem(this.store, task.id, round.workItemId);
@@ -1716,7 +1771,7 @@ export class FileTaskWorkspacePreparer {
1716
1771
  async #reassignTaskReviewWorkspace(task, round, reviewer, reviewRoot, frozenEntries) {
1717
1772
  const previous = this.store.listReviewRounds(task.id)
1718
1773
  .filter((candidate) => (candidate.id !== round.id
1719
- && (candidate.scope ?? "work-item") === "task"
1774
+ && candidate.scope === "task"
1720
1775
  && candidate.reviewerRoleName === round.reviewerRoleName
1721
1776
  && (candidate.status === "completed" || candidate.status === "failed")
1722
1777
  && candidate.workspaceDisposition?.kind !== "removed"
@@ -1878,6 +1933,7 @@ export class FileTaskWorkspacePreparer {
1878
1933
  if (!isDeepStrictEqual(round.workspace, workspace)) {
1879
1934
  throw new Error(`ReviewRound workspace record diverged: ${round.id}.`);
1880
1935
  }
1936
+ await this.#assertWorkspaceCleanup(workspace, "abandoned");
1881
1937
  assertWorkspaceSessionsRetirable(this.store, task.id, round.reviewerRoleName, this.now());
1882
1938
  for (const lane of round.executionGroup?.lanes ?? []) {
1883
1939
  assertWorkspaceSessionsRetirable(this.store, task.id, lane.roleName, this.now());
@@ -1945,6 +2001,7 @@ export class FileTaskWorkspacePreparer {
1945
2001
  throw new Error(`WorkItem has no managed isolated worktree workspace: ${item.id}.`);
1946
2002
  }
1947
2003
  assertWorkItemOwnsWorkspace(item, workspace);
2004
+ await this.#assertWorkspaceCleanup(workspace, disposition);
1948
2005
  if (item.assignee !== undefined) {
1949
2006
  assertWorkspaceSessionsRetirable(this.store, task.id, item.assignee, this.now());
1950
2007
  }
@@ -1998,7 +2055,7 @@ export class FileTaskWorkspacePreparer {
1998
2055
  let found = false;
1999
2056
  for (const entry of workspace.entries.filter(({ access }) => access === "write")) {
2000
2057
  try {
2001
- if (!await this.git.isClean(entry.path))
2058
+ if (!await this.git.inspectClean(entry.path))
2002
2059
  return "dirty";
2003
2060
  found = true;
2004
2061
  }
@@ -2010,7 +2067,7 @@ export class FileTaskWorkspacePreparer {
2010
2067
  }
2011
2068
  return found ? "clean" : "missing";
2012
2069
  }
2013
- async cleanupIntegrationWorkspace(taskId, integrationId) {
2070
+ async cleanupIntegrationWorkspace(taskId, integrationId, options = {}) {
2014
2071
  const task = requireTask(this.store, taskId);
2015
2072
  const attempt = this.store.getIntegrationAttempt(task.id, integrationId);
2016
2073
  if (attempt === null) {
@@ -2023,6 +2080,11 @@ export class FileTaskWorkspacePreparer {
2023
2080
  throw new Error(`IntegrationAttempt must be terminal before cleanup: ${attempt.id}.`);
2024
2081
  }
2025
2082
  const workspace = this.store.getIntegrationWorkspace(task.id, attempt.id);
2083
+ // A failed Integration's explicitly disposable conflict tree retains its
2084
+ // existing cleanup contract outside archive. Archive never discards dirt.
2085
+ if (workspace !== null && (options.preserveChanges || task.status === "archived" || attempt.status !== "failed")) {
2086
+ await this.#assertWorkspaceCleanup(workspace, "abandoned");
2087
+ }
2026
2088
  const project = requireProject(this.store, attempt.projectId);
2027
2089
  // The recorded entry pins the exact on-disk worktree path; reconstruct the
2028
2090
  // owner root from it so removal targets the real Git worktree regardless of
@@ -2042,7 +2104,7 @@ export class FileTaskWorkspacePreparer {
2042
2104
  directory,
2043
2105
  taskSegment: this.#taskSegment(task),
2044
2106
  integrationId: attempt.id,
2045
- discardChanges: task.status !== "archived" && attempt.status === "failed"
2107
+ discardChanges: options.preserveChanges !== true && task.status !== "archived" && attempt.status === "failed"
2046
2108
  });
2047
2109
  if (result !== "dirty") {
2048
2110
  for (const entry of workspace?.entries ?? []) {
@@ -2067,9 +2129,11 @@ export class FileTaskWorkspacePreparer {
2067
2129
  if (main.owner.type !== "task") {
2068
2130
  throw new Error(`Task main workspace ownership is invalid: ${task.id}.`);
2069
2131
  }
2132
+ if (main.entries.length === 0)
2133
+ return "missing";
2070
2134
  return this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries);
2071
2135
  }
2072
- async cleanupTaskForArchive(taskId) {
2136
+ async cleanupTaskForArchive(taskId, disposition = "abandoned") {
2073
2137
  const task = requireTask(this.store, taskId);
2074
2138
  assertTaskArchiveState(task, task);
2075
2139
  const isolated = this.store.listManagedWorkspaces(task.id)
@@ -2084,8 +2148,10 @@ export class FileTaskWorkspacePreparer {
2084
2148
  }
2085
2149
  const main = this.store.getTaskWorkspace(task.id);
2086
2150
  if (main !== null) {
2151
+ await this.#assertWorkspaceCleanup(main, disposition);
2087
2152
  assertTaskArchiveState(requireTask(this.store, task.id), task);
2088
- if (await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
2153
+ // Scratch-only workspaces have no Git entries or Git workspace identity.
2154
+ if (main.entries.length > 0 && await this.#inspectEntries(task.id, this.#taskSegment(task), MAIN_WORKTREE, main.entries) === "dirty") {
2089
2155
  return {
2090
2156
  taskId,
2091
2157
  status: "retained-dirty",
@@ -2166,7 +2232,7 @@ export class FileTaskWorkspacePreparer {
2166
2232
  return integrationWorkspaceRoot(this.home, taskId, integrationId);
2167
2233
  }
2168
2234
  #reviewWorktreeName(round) {
2169
- return (round.scope ?? "work-item") === "task"
2235
+ return round.scope === "task"
2170
2236
  ? `reviewer-${round.reviewerRoleName}`
2171
2237
  : round.id;
2172
2238
  }
@@ -0,0 +1,187 @@
1
+ import { dirname, isAbsolute, join, relative } from "node:path";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
4
+ import { cleanupCheckFromError } from "../workspace/cleanupInspection.js";
5
+ import { WorkItemChangeSetManager } from "../workspace/workItemChangeSetManager.js";
6
+ import { managedWorkspaceKey, managedWorktreeName } from "../worktree/managedWorkspace.js";
7
+ import { integrationWorktreeIdentity, worktreeIdentity } from "./gitWorkspace.js";
8
+ import { taskWorkspaceRefSegment } from "./taskWorkspaceIdentity.js";
9
+ /** Current owner facts + the same Git inspection used immediately before removal.
10
+ * No preparation, locks, Git refresh, runtime stop, DB writes or repair.
11
+ */
12
+ export async function inspectWorkspaceCleanup(store, git, workspace, disposition, forceArchive = false) {
13
+ const owner = workspace.owner;
14
+ const task = store.getTask(owner.taskId);
15
+ if (task === null)
16
+ throw new Error(`Task not found: ${owner.taskId}.`);
17
+ const resource = managedWorkspaceKey(owner);
18
+ const sources = [resource];
19
+ const actions = workspaceCleanupActions(workspace);
20
+ const checks = [];
21
+ const add = (reason, detail, expected, observed) => checks.push({ resource, reason, status: "blocked", detail, expected, observed, sources, actions });
22
+ if (!isDeepStrictEqual(store.getManagedWorkspace(owner), workspace)) {
23
+ add("workspace-identity-mismatch", "Current workspace ownership differs from the inspected record.", "same managed workspace", "changed or missing");
24
+ return checks;
25
+ }
26
+ if (task.workspaceIdentity === undefined && workspace.entries.some(e => e.access === "write")) {
27
+ add("workspace-metadata-missing", "Task has no durable workspace identity; do not infer one from paths.", "recorded Task workspace identity", null);
28
+ return checks;
29
+ }
30
+ let roleName = managedWorktreeName(owner);
31
+ switch (owner.type) {
32
+ case "work-item": {
33
+ const item = store.getWorkItem(task.id, owner.workItemId);
34
+ if (item === null || !["accepted", "retired"].includes(item.status)) {
35
+ add("owner-unsettled", "WorkItem must be terminal before cleanup.", ["accepted", "retired"], item?.status ?? null);
36
+ }
37
+ if (item?.workspaceDisposition !== undefined && item.workspaceDisposition !== disposition) {
38
+ add("workspace-disposition-mismatch", "WorkItem cleanup disposition differs from the recorded decision.", item.workspaceDisposition, disposition);
39
+ }
40
+ if (item?.status === "accepted" && disposition === "integrated") {
41
+ const inspection = await new WorkItemChangeSetManager(store).inspectIntegrated(task.id, item.id);
42
+ checks.push(...inspection.checks);
43
+ }
44
+ break;
45
+ }
46
+ case "review-round": {
47
+ const round = store.getReviewRound(task.id, owner.reviewRoundId);
48
+ if (round === null || !["completed", "failed"].includes(round.status)) {
49
+ add("owner-unsettled", "ReviewRound must be terminal before cleanup.", ["completed", "failed"], round?.status ?? null);
50
+ }
51
+ if (round?.workspace === undefined || !isDeepStrictEqual(round.workspace, workspace)) {
52
+ add("workspace-metadata-mismatch", "ReviewRound and managed workspace records diverge.", "same workspace in ReviewRound and registry", "different or missing mirror");
53
+ }
54
+ if (round?.scope === "task")
55
+ roleName = `reviewer-${round.reviewerRoleName}`;
56
+ break;
57
+ }
58
+ case "execution-lane": {
59
+ const group = owner.purpose === "execution"
60
+ ? store.getWorkItem(task.id, owner.workItemId)?.executionGroups.find(g => g.id === owner.executionGroupId)
61
+ : store.getReviewRound(task.id, owner.reviewRoundId)?.executionGroup;
62
+ const lane = group?.id === owner.executionGroupId
63
+ ? group.lanes.find(l => l.id === owner.executionLaneId) : undefined;
64
+ if (lane === undefined || lane.disposition === "open") {
65
+ add("owner-unsettled", "Execution Lane is not terminally resolved.", "settled lane", lane?.disposition ?? null);
66
+ }
67
+ break;
68
+ }
69
+ case "integration-attempt": {
70
+ const attempt = store.getIntegrationAttempt(task.id, owner.integrationAttemptId);
71
+ if (attempt === null || ["running", "blocked", "conflicted", "validating"].includes(attempt.status)) {
72
+ add("owner-unsettled", "IntegrationAttempt must be terminal before cleanup.", "terminal attempt", attempt?.status ?? null);
73
+ }
74
+ break;
75
+ }
76
+ case "task":
77
+ if (!["completed", "cancelled", "archived"].includes(task.status)) {
78
+ add("task-not-terminal", "Task must be completed or retired before archive cleanup.", ["completed", "cancelled", "archived"], task.status);
79
+ }
80
+ break;
81
+ }
82
+ const delivery = owner.type === "task" && (disposition === "integrated" || task.status === "archived" || forceArchive)
83
+ ? await readArchiveDelivery(store, git, task, forceArchive) : undefined;
84
+ if (delivery !== undefined && (!delivery.allMerged || !delivery.allVerified)) {
85
+ add("delivery-coverage", "Task main has commits without verified remote coverage; cleanup does not establish delivery.", { allMerged: true, allVerified: true }, { allMerged: delivery.allMerged, allVerified: delivery.allVerified });
86
+ }
87
+ for (const entry of workspace.entries.filter(e => e.access === "write")) {
88
+ const entryResource = `${resource}/${entry.projectId}`;
89
+ const entryChecks = [];
90
+ const check = (reason, detail, expected, observed) => entryChecks.push({ resource: entryResource, reason, status: "blocked", detail, expected, observed, sources, actions });
91
+ // Integration's durable root is its single Project worktree (not the
92
+ // multi-Project owner container used by the other workspace owners).
93
+ const container = owner.type === "integration-attempt" ? dirname(workspace.root) : workspace.root;
94
+ const expectedPath = join(container, entry.directory);
95
+ if (entry.path !== expectedPath) {
96
+ check("workspace-path-mismatch", "Recorded path is not the current owner-root/Project path; relocation is not proven.", { ownerRelativePath: entry.directory }, { ownerRelativePath: safeRelativePath(container, entry.path) });
97
+ checks.push(...entryChecks);
98
+ continue; // Never inspect a foreign/mismatched path.
99
+ }
100
+ const taskSegment = taskWorkspaceRefSegment(task);
101
+ const expectedBranch = owner.type === "integration-attempt"
102
+ ? integrationWorktreeIdentity(taskSegment, owner.integrationAttemptId).branch
103
+ : worktreeIdentity(taskSegment, roleName).branch;
104
+ if (entry.branch !== expectedBranch) {
105
+ check("workspace-identity-mismatch", "Recorded branch does not match the exact owner.", expectedBranch, entry.branch);
106
+ checks.push(...entryChecks);
107
+ continue;
108
+ }
109
+ try {
110
+ const main = store.getTaskWorkspace(task.id)?.entries.find(e => e.projectId === entry.projectId);
111
+ if (main === undefined && owner.type !== "task") {
112
+ check("workspace-metadata-missing", "Task main Git repository reference is unavailable.", "Task main Project entry", null);
113
+ }
114
+ else {
115
+ const state = owner.type === "task"
116
+ ? await git.inspectTaskClone({ path: entry.path, container: workspace.root, directory: entry.directory,
117
+ taskSegment, branch: entry.branch })
118
+ : await git.inspectWorktree({ repositoryPath: main.path, container, directory: entry.directory,
119
+ taskSegment, roleName, expectedBranch });
120
+ if (state === "dirty")
121
+ check("dirty-worktree", "Uncommitted or untracked changes must be preserved.", "clean", "dirty");
122
+ if (owner.type === "task" && state !== "missing" && delivery !== undefined) {
123
+ const covered = delivery.projects.find(p => p.projectId === entry.projectId);
124
+ const expected = covered?.expectedLocalCommit;
125
+ const observed = (await git.inspect(entry.path, "HEAD")).baseCommit;
126
+ if (expected != null && expected !== observed && covered?.deliveryLocalCommit !== observed) {
127
+ check("head-mismatch", "Task main HEAD differs from the accepted head and its covered publication candidate.", { acceptedCommit: expected, deliveryLocalCommit: covered?.deliveryLocalCommit ?? null }, observed);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ catch (error) {
133
+ entryChecks.push(...cleanupCheckFromError(error, entryResource, sources, actions));
134
+ }
135
+ checks.push(...entryChecks);
136
+ }
137
+ return checks;
138
+ }
139
+ /** Reuse Publication's projection, supplying live heads only where its existing
140
+ * contract asks for them (active/cancelled). Completed/archived heads stay frozen.
141
+ * Never prepare a workspace or substitute an old binding's commit for a failed read.
142
+ */
143
+ export async function readArchiveDelivery(store, git, task, forceArchive = false) {
144
+ const projects = [];
145
+ // Force admission preserves its existing store-only evidence; it never
146
+ // freezes new delivery heads merely because an inspection observed them.
147
+ if (!forceArchive && (task.status === "active" || task.status === "cancelled") && task.workspaceIdentity !== undefined) {
148
+ const main = store.getTaskWorkspace(task.id);
149
+ if (main?.owner.type === "task" && main.owner.taskId === task.id) {
150
+ const branch = worktreeIdentity(taskWorkspaceRefSegment(task), "main").branch;
151
+ for (const entry of main.entries.filter(e => e.access === "write")) {
152
+ if (entry.path !== join(main.root, entry.directory) || entry.branch !== branch)
153
+ continue;
154
+ try {
155
+ const head = await git.inspect(entry.path, "HEAD");
156
+ if (head.root === entry.path && await git.headRef(entry.path) === branch) {
157
+ projects.push({ projectId: entry.projectId, commit: head.baseCommit });
158
+ }
159
+ }
160
+ catch { /* Failed reads remain unavailable in the existing projection. */ }
161
+ }
162
+ }
163
+ }
164
+ return projectTaskRemoteDeliveryFromStore(store, task, { projects });
165
+ }
166
+ export function workspaceCleanupActions(workspace) {
167
+ const owner = workspace.owner;
168
+ switch (owner.type) {
169
+ case "task": return [`yui task remote-delivery ${owner.taskId}`, `yui task show ${owner.taskId}`];
170
+ case "work-item": return [`yui task work show ${owner.taskId}/${owner.workItemId}`,
171
+ `yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`];
172
+ case "review-round": return [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
173
+ `yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
174
+ case "integration-attempt": return [`yui task integration show ${owner.taskId}/${owner.integrationAttemptId}`,
175
+ `yui task integration cleanup ${owner.taskId}/${owner.integrationAttemptId}`];
176
+ case "execution-lane": return owner.purpose === "execution"
177
+ ? [`yui task work show ${owner.taskId}/${owner.workItemId}`,
178
+ `yui task work cleanup ${owner.taskId}/${owner.workItemId} --integrated|--abandon`]
179
+ : [`yui task work review show ${owner.taskId}/${owner.reviewRoundId}`,
180
+ `yui task work review cleanup ${owner.taskId}/${owner.reviewRoundId}`];
181
+ }
182
+ }
183
+ /** Only expose paths inside this already-authorized owner, never foreign paths. */
184
+ export function safeRelativePath(root, path) {
185
+ const value = relative(root, path);
186
+ return value === ".." || value.startsWith("../") || isAbsolute(value) ? "[outside owner root]" : value || ".";
187
+ }