@zq-silk/yui 0.15.12 → 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 (236) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/ARCHITECTURE.zh-CN.md +3 -2
  3. package/dist/artifacts/artifactCommitLock.js +16 -55
  4. package/dist/artifacts/taskArtifactRepository.js +3 -3
  5. package/dist/cli/agentConfigurationPicker.js +1 -1
  6. package/dist/cli/commandCatalog.js +57 -41
  7. package/dist/cli/completion.js +18 -18
  8. package/dist/cli/completionWizard.js +1 -1
  9. package/dist/cli/dynamicCompletion.js +1 -1
  10. package/dist/cli/interactionPolicy.js +7 -3
  11. package/dist/cli/invocationAuthority.js +64 -0
  12. package/dist/cli/managedDiagnostics.js +1 -1
  13. package/dist/cli/parseRepeatable.js +35 -0
  14. package/dist/cli/updatePorts.js +26 -5
  15. package/dist/cli.js +1280 -1243
  16. package/dist/commands/agentCommands.js +4 -4
  17. package/dist/commands/capabilityCommands.js +1 -1
  18. package/dist/commands/configCommands.js +6 -57
  19. package/dist/commands/configOverview.js +2 -2
  20. package/dist/commands/durableJobCommands.js +12 -6
  21. package/dist/commands/globalRoleCommands.js +53 -29
  22. package/dist/commands/grantCommands.js +0 -3
  23. package/dist/commands/jobCommands.js +1 -4
  24. package/dist/commands/operatorCommands.js +3 -3
  25. package/dist/commands/projectCommands.js +1 -10
  26. package/dist/commands/resourcesCommands.js +10 -40
  27. package/dist/commands/roleConfiguration.js +2 -6
  28. package/dist/commands/taskActivationCommands.js +4 -8
  29. package/dist/commands/taskAgentCapabilities.js +9 -0
  30. package/dist/commands/taskCommandSupport.js +155 -0
  31. package/dist/commands/taskCommandTypes.js +1 -0
  32. package/dist/commands/taskCommands.js +271 -710
  33. package/dist/commands/taskCompletionGate.js +1 -1
  34. package/dist/commands/taskExecutionCommands.js +1 -1
  35. package/dist/commands/taskFactCommands.js +325 -0
  36. package/dist/commands/taskInputCommands.js +12 -42
  37. package/dist/commands/taskIntegrationCommands.js +17 -57
  38. package/dist/commands/taskNextActionCommand.js +7 -6
  39. package/dist/commands/taskPublicationAdoptCommand.js +3 -3
  40. package/dist/commands/taskPublicationCommands.js +1 -1
  41. package/dist/commands/taskPublicationVerifyCommand.js +1 -1
  42. package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
  43. package/dist/commands/taskUpstreamCommands.js +19 -8
  44. package/dist/commands/workflowCommands.js +6 -1
  45. package/dist/completion/completionInstaller.js +26 -26
  46. package/dist/completion/completionState.js +26 -26
  47. package/dist/completion/fileCompletionManager.js +6 -11
  48. package/dist/config/configCatalog.js +0 -2
  49. package/dist/config/timeZone.js +14 -0
  50. package/dist/config/yuiConfig.js +0 -29
  51. package/dist/context/runContextPack.js +2 -3
  52. package/dist/context/taskCatalog.js +3 -5
  53. package/dist/context/taskContext.js +139 -151
  54. package/dist/context/wakeRunReferences.js +11 -0
  55. package/dist/controller/agentCapabilities.js +58 -0
  56. package/dist/controller/agentRuntimeObserver.js +6 -11
  57. package/dist/controller/clientRuntime.js +22 -14
  58. package/dist/controller/controller.js +9 -22
  59. package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
  60. package/dist/controller/jobClient.js +4 -4
  61. package/dist/controller/jobControl.js +60 -25
  62. package/dist/controller/jobSupervisor.js +4 -4
  63. package/dist/controller/providerRetryDelivery.js +7 -7
  64. package/dist/controller/runtime.js +51 -44
  65. package/dist/controller/runtimeEventProcessor.js +0 -5
  66. package/dist/controller/sessionOwnerReconciliation.js +5 -0
  67. package/dist/controller/taskAgentError.js +35 -0
  68. package/dist/coordination/workMailboxQueue.js +2 -24
  69. package/dist/core/controllerClient.js +2 -2
  70. package/dist/core/fileLockOwner.js +74 -0
  71. package/dist/decision/decision.js +17 -0
  72. package/dist/doctor/doctor.js +18 -9
  73. package/dist/event/taskEvent.js +12 -0
  74. package/dist/execution/workItemExecutionProjection.js +2 -2
  75. package/dist/executor/agentCapabilityConfig.js +43 -0
  76. package/dist/executor/agentConfigurationCatalog.js +49 -11
  77. package/dist/executor/agentExecutor.js +5 -8
  78. package/dist/executor/fileRoleLaunchPlanner.js +11 -53
  79. package/dist/executor/taskAgentCapabilities.js +64 -0
  80. package/dist/integration/deliveryObligation.js +1 -69
  81. package/dist/integration/gitIntegrationService.js +60 -84
  82. package/dist/integration/integrationAttempt.js +3 -14
  83. package/dist/integration/integrationSourceApplication.js +4 -12
  84. package/dist/integration/manifestTags.js +2 -2
  85. package/dist/job/durableJob.js +4 -25
  86. package/dist/job/jobAssignmentScope.js +22 -0
  87. package/dist/job/jobOperation.js +16 -0
  88. package/dist/job/jobRunner.js +2 -1
  89. package/dist/job/stepDirectory.js +15 -0
  90. package/dist/kernel/builtinCapabilities.js +15 -11
  91. package/dist/kernel/kernelPorts.js +1 -17
  92. package/dist/lifecycle/exactRunTerminalization.js +1 -9
  93. package/dist/message/inputControlResolution.js +3 -4
  94. package/dist/message/message.js +17 -20
  95. package/dist/message/messageContinuation.js +1 -1
  96. package/dist/milestone/milestone.js +11 -0
  97. package/dist/observability/orchestrationMetrics.js +24 -1
  98. package/dist/observability/runtimeIdentity.js +1 -17
  99. package/dist/output/agentConfigurationPresentation.js +2 -0
  100. package/dist/output/timePresentation.js +1 -14
  101. package/dist/plugins/pluginService.js +12 -4
  102. package/dist/release/releaseWorkflowPorts.js +45 -96
  103. package/dist/release/runtimeRelease.js +9 -15
  104. package/dist/repository/projectMaintenanceLock.js +23 -64
  105. package/dist/repository/taskBaseFreshness.js +1 -1
  106. package/dist/repository/taskWorkspaceCoordinator.js +13 -22
  107. package/dist/repository/taskWorkspacePreparer.js +29 -25
  108. package/dist/repository/workspaceCleanupInspection.js +5 -5
  109. package/dist/resources/autoResourceGc.js +4 -77
  110. package/dist/resources/liveReferences.js +7 -54
  111. package/dist/resources/resourceDiscovery.js +5 -118
  112. package/dist/resources/resourceGc.js +233 -181
  113. package/dist/resources/resourceRegistrar.js +5 -4
  114. package/dist/resources/resourceRegistry.js +3 -37
  115. package/dist/resources/resourceRegistryStore.js +13 -36
  116. package/dist/resources/sqliteResourceRegistry.js +43 -26
  117. package/dist/review/deltaRecheck.js +1 -1
  118. package/dist/review/reviewAcceptance.js +1 -1
  119. package/dist/review/reviewDecision.js +1 -1
  120. package/dist/review/reviewRound.js +8 -7
  121. package/dist/review/taskFinalReviewContractResolution.js +1 -1
  122. package/dist/runtime/acpProtocol.js +4 -51
  123. package/dist/runtime/acpSession.js +10 -75
  124. package/dist/runtime/acpSessionConfiguration.js +1 -8
  125. package/dist/runtime/agentEndpoint.js +3 -2
  126. package/dist/runtime/agentFailureContext.js +43 -0
  127. package/dist/runtime/agentHost.js +15 -15
  128. package/dist/runtime/codexInteractiveHost.js +2 -2
  129. package/dist/runtime/index.js +0 -1
  130. package/dist/runtime/managedCaller.js +21 -1
  131. package/dist/runtime/providerErrors.js +38 -0
  132. package/dist/runtime/runtimeObservation.js +0 -5
  133. package/dist/runtime/runtimeSessionCandidate.js +2 -3
  134. package/dist/runtime/sessionOwnerIdentity.js +1 -1
  135. package/dist/runtime/structuredProviderHost.js +4 -41
  136. package/dist/runtime/tmuxAdapters.js +6 -8
  137. package/dist/scheduler/activeRoleRunDelivery.js +7 -7
  138. package/dist/scheduler/leaderWakeupProcessor.js +19 -6
  139. package/dist/scheduler/operatorEvent.js +12 -20
  140. package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
  141. package/dist/scheduler/ports.js +5 -8
  142. package/dist/scheduler/roleRunLiveness.js +4 -4
  143. package/dist/scheduler/roleRunStall.js +16 -23
  144. package/dist/scheduler/taskExecutionProjection.js +25 -33
  145. package/dist/scheduler/taskObservabilityProjection.js +0 -1
  146. package/dist/scheduler/taskWake.js +5 -10
  147. package/dist/scheduler/wakeReason.js +2 -0
  148. package/dist/scheduler/wakeupQueue.js +2 -4
  149. package/dist/setup/setupCommand.js +1 -1
  150. package/dist/storage/contextRecords.js +106 -0
  151. package/dist/storage/currentTaskStore.js +1 -1
  152. package/dist/storage/homeLayout.js +13 -17
  153. package/dist/storage/migrations/agentFailureContext.js +22 -0
  154. package/dist/storage/migrations/currentInputContract.js +86 -0
  155. package/dist/storage/migrations/currentRuntimeContract.js +228 -0
  156. package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
  157. package/dist/storage/migrations/integrationContinuation.js +5 -4
  158. package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
  159. package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
  160. package/dist/storage/migrations/verificationPlanV1.js +162 -0
  161. package/dist/storage/migrations/verificationPolicy.js +74 -0
  162. package/dist/storage/migrations/workItemHistory.js +46 -0
  163. package/dist/storage/persistenceWorker.js +12 -4
  164. package/dist/storage/recordValidation.js +87 -0
  165. package/dist/storage/sqliteSchema.js +142 -1
  166. package/dist/storage/sqliteStore.js +156 -162
  167. package/dist/storage/storageSchema.js +2 -14
  168. package/dist/storage/storageVersions.js +1 -1
  169. package/dist/storage/storeRpc.js +15 -7
  170. package/dist/storage/taskStore.js +3 -7
  171. package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
  172. package/dist/task/completionReadiness.js +5 -24
  173. package/dist/task/draftPlan.js +0 -53
  174. package/dist/task/nextAction.js +7 -13
  175. package/dist/task/remoteDeliveryService.js +89 -0
  176. package/dist/task/taskActivation.js +2 -25
  177. package/dist/task/taskActivationService.js +0 -2
  178. package/dist/task/taskRecordReference.js +0 -1
  179. package/dist/task/taskSubmission.js +3 -14
  180. package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
  181. package/dist/telemetry/sqliteTelemetryStore.js +49 -49
  182. package/dist/telemetry/telemetryWiring.js +4 -3
  183. package/dist/tmux/tmuxManager.js +29 -20
  184. package/dist/verification/gateArtifact.js +15 -24
  185. package/dist/verification/gateArtifactStore.js +14 -7
  186. package/dist/verification/verificationGateService.js +29 -88
  187. package/dist/verification/verificationPlan.js +27 -72
  188. package/dist/web/assets/client/app.js +1 -1
  189. package/dist/web/assets/client/taskSurface.js +4 -4
  190. package/dist/web/webServer.js +4 -6
  191. package/dist/web/webSnapshot.js +3 -83
  192. package/dist/web/webTaskSurface.js +17 -46
  193. package/dist/workItem/workItem.js +1 -12
  194. package/docs/architecture/capabilities-and-resources.md +14 -2
  195. package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
  196. package/docs/managed-turn-and-session-runtime.md +83 -10
  197. package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
  198. package/docs/plugin-sdk.md +4 -2
  199. package/docs/provider-runtime.md +3 -1
  200. package/docs/provider-runtime.zh-CN.md +4 -2
  201. package/docs/release-workflow.md +190 -7
  202. package/docs/release-workflow.zh-CN.md +139 -5
  203. package/docs/roles-and-configuration.md +29 -0
  204. package/docs/roles-and-configuration.zh-CN.md +21 -0
  205. package/docs/sqlite-control-plane-design.md +16 -3
  206. package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
  207. package/docs/task-dag-semantics.md +3 -3
  208. package/docs/task-dag-semantics.zh-CN.md +2 -2
  209. package/docs/task-delivery.md +91 -5
  210. package/docs/task-delivery.zh-CN.md +72 -4
  211. package/docs/task-discovery.md +10 -11
  212. package/docs/task-discovery.zh-CN.md +8 -9
  213. package/docs/testing/verification-levels.md +145 -9
  214. package/docs/testing/verification-levels.zh-CN.md +103 -8
  215. package/package.json +1 -1
  216. package/skills/yui-leader/SKILL.md +5 -0
  217. package/skills/yui-leader/references/integration.md +9 -5
  218. package/skills/yui-leader/references/planning.md +9 -3
  219. package/skills/yui-operator/SKILL.md +15 -10
  220. package/skills/yui-runtime/SKILL.md +6 -6
  221. package/skills/yui-runtime/references/recovery.md +47 -0
  222. package/dist/agent/agentRegistry.js +0 -10
  223. package/dist/agentRun/runIdentity.js +0 -22
  224. package/dist/commands/deliveryGuardPreflight.js +0 -30
  225. package/dist/commands/taskIntegrationQueueCommands.js +0 -226
  226. package/dist/commands/taskOverviewCommand.js +0 -377
  227. package/dist/completion/completionWizard.js +0 -125
  228. package/dist/context/dispatchContext.js +0 -110
  229. package/dist/context/wakeNotification.js +0 -144
  230. package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
  231. package/dist/integration/integrationQueueEntry.js +0 -191
  232. package/dist/integration/integrationQueueService.js +0 -810
  233. package/dist/release/fakeReleasePorts.js +0 -55
  234. package/dist/runtime/sessionOwnerRegistry.js +0 -137
  235. package/dist/task/deliveryGuard.js +0 -226
  236. /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
package/dist/cli.js CHANGED
@@ -1,117 +1,121 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
- import { recordTaskInterruptResult } from "./message/taskInterrupt.js";
5
- import { agentAdapterLabel as adapterLabel } from "./agent/adapterCatalog.js";
6
4
  import { readFileSync } from "node:fs";
7
5
  import { resolve } from "node:path";
8
6
  import { createInterface } from "node:readline/promises";
9
7
  import { fileURLToPath } from "node:url";
10
8
  import { isDeepStrictEqual } from "node:util";
11
- import { renderCommandHelp } from "./cli/helpRenderer.js";
9
+ import { agentAdapterLabel as adapterLabel } from "./agent/adapterCatalog.js";
10
+ import { recordTaskInterruptResult } from "./message/taskInterrupt.js";
11
+ import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
12
+ import { listArtifactsCapability, readArtifactCapability, saveArtifactCapability } from "./artifacts/artifactCapability.js";
13
+ import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
12
14
  import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
13
- import { routeInvocation } from "./cli/invocationRouter.js";
14
15
  import { renderCompletion } from "./cli/completion.js";
16
+ import { runCompletionWizard } from "./cli/completionWizard.js";
15
17
  import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
16
- import { recordGlobalInterruptResult, recordGlobalSteerResult } from "./message/globalInterrupt.js";
18
+ import { renderCommandHelp } from "./cli/helpRenderer.js";
17
19
  import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.js";
18
- import { runCompletionWizard } from "./cli/completionWizard.js";
19
- import { renderAgentConfigurationResolutionNotice } from "./cli/agentConfigurationPicker.js";
20
- import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
20
+ import { routeInvocation } from "./cli/invocationRouter.js";
21
+ import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
22
+ import { assertConfigurationAuthority, assertTaskInvocationScope } from "./cli/invocationAuthority.js";
23
+ import { requireManagedGlobalCaller } from "./runtime/managedCaller.js";
21
24
  import { resolveOperatorWizardArguments } from "./cli/operatorWizard.js";
25
+ import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
22
26
  import { runUpdateCommand } from "./cli/updateCommand.js";
27
+ import { createUpdatePorts } from "./cli/updatePorts.js";
23
28
  import { runUpgradeCommand } from "./cli/upgradeCommand.js";
24
- import { formatTimestamp } from "./output/timePresentation.js";
25
- import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
26
- import { renderAgentConfigurationCatalog } from "./output/agentConfigurationPresentation.js";
27
- import { nativeAgentEnvironmentNames } from "./agent/launchEnvironment.js";
28
29
  import { runAgentCommand } from "./commands/agentCommands.js";
29
- import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
30
- import { runConfigCommand } from "./commands/configCommands.js";
31
30
  import { runCapabilityCommand } from "./commands/capabilityCommands.js";
32
- import { CONFIG_DOMAINS } from "./config/configCatalog.js";
31
+ import { runConfigCommand } from "./commands/configCommands.js";
33
32
  import { runConfigOverview } from "./commands/configOverview.js";
34
- import { parseControllerCleanupOptions, parseControllerStatusOptions, parseControllerRuntimeSnapshot, renderControllerResourceStatus, renderRuntimeIdentitySection, summarizeDurablePhysicalMismatch, runInteractiveControllerCleanup } from "./commands/controllerCommands.js";
33
+ import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
34
+ import { runDurableJobCommand } from "./commands/durableJobCommands.js";
35
35
  import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
36
- import { parseSessionReconcileOptions, parseSessionStopOptions, runSessionReconcileCommand, runSessionStopCommand } from "./commands/sessionCommands.js";
37
- import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
36
+ import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
38
37
  import { runJobCommand } from "./commands/jobCommands.js";
39
- import { runDurableJobCommand } from "./commands/durableJobCommands.js";
40
- import { runTelemetryCommand } from "./commands/telemetryCommands.js";
41
- import { runResourcesCommand } from "./commands/resourcesCommands.js";
42
38
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
43
- import { runProjectCommand } from "./commands/projectCommands.js";
44
39
  import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
45
- import { assertWorkItemDependenciesCompletedForCommand, requireWorkItemAssignee, dispatchPreparedReviewRound, failPendingReviewRound, preserveReviewRoundWorkspace, parseTaskCompletionRequest, previewTaskRoleAgentConfigurationMutation, preflightTaskCompletion, runTaskCommand, planReplicatedWorkItemLanes, validateTaskArchiveRequest, parseTaskArchiveArguments } from "./commands/taskCommands.js";
46
- import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./commands/taskRemoteDeliveryCommand.js";
47
- import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
48
- import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
49
- import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
50
- import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
51
- import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
52
- import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
53
- import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
54
- import { taskLocalActor, assertTaskDeliveryAuthority } from "./commands/taskActor.js";
55
- import { saveArtifactCapability, readArtifactCapability, listArtifactsCapability } from "./artifacts/artifactCapability.js";
56
- import { parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, finalizeStoppedTaskExecution, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
57
- import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
40
+ import { runProjectCommand } from "./commands/projectCommands.js";
41
+ import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
42
+ import { runResourcesCommand } from "./commands/resourcesCommands.js";
43
+ import { parseSessionReconcileOptions, parseSessionStopOptions, runSessionReconcileCommand, runSessionStopCommand } from "./commands/sessionCommands.js";
44
+ import { assertTaskDeliveryAuthority, taskLocalActor } from "./task/taskAuthority.js";
45
+ import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
58
46
  import { runTaskChangeSetCommand } from "./commands/taskChangeSetCommands.js";
47
+ import { assertWorkItemDependenciesCompletedForCommand, dispatchPreparedReviewRound, failPendingReviewRound, parseTaskArchiveArguments, parseTaskCompletionRequest, planReplicatedWorkItemLanes, preflightTaskCompletion, preserveReviewRoundWorkspace, previewTaskRoleAgentConfigurationMutation, requireWorkItemAssignee, runTaskCommand, validateTaskArchiveRequest } from "./commands/taskCommands.js";
48
+ import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
49
+ import { finalizeStoppedTaskExecution, parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
50
+ import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
59
51
  import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
60
- import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
52
+ import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
53
+ import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
54
+ import { runTaskUpstreamCommand } from "./commands/taskUpstreamCommands.js";
61
55
  import { runTaskWorkspaceCommand } from "./commands/taskWorkspaceCommands.js";
56
+ import { runTelemetryCommand } from "./commands/telemetryCommands.js";
62
57
  import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
63
- import { createUpdatePorts } from "./cli/updatePorts.js";
64
- import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
65
- import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
66
- import { assertCliHomeReleaseFence, describeCliHomeInvocation } from "./release/cliHomeReleaseFence.js";
67
- import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
68
- import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
69
- import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
70
- import { runTaskUpstreamCommand } from "./commands/taskUpstreamCommands.js";
71
- import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
72
- import { FileCompletionManager, resolveCliIdentity } from "./completion/fileCompletionManager.js";
58
+ import { FileCompletionManager } from "./completion/fileCompletionManager.js";
59
+ import { CONFIG_DOMAINS } from "./config/configCatalog.js";
60
+ import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
61
+ import { readSessionBootstrapManifest, refreshManagedSessionCliWrappers } from "./context/sessionBootstrapManifest.js";
73
62
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
74
- import { callController, ControllerClientError } from "./core/controllerClient.js";
75
63
  import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
64
+ import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
76
65
  import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
77
66
  import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
78
- import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
79
- import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
80
67
  import { runRuntimeObservationHookCommand } from "./controller/runtimeObservationHook.js";
68
+ import { runSessionNotifyCommand } from "./controller/sessionNotify.js";
69
+ import { SessionOwnerReconciliation } from "./controller/sessionOwnerReconciliation.js";
70
+ import { callController, ControllerClientError } from "./core/controllerClient.js";
81
71
  import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
82
72
  import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
73
+ import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "./executor/agentConfigurationCatalog.js";
83
74
  import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
84
- import { AGENT_HOST_CONTROL_PROTOCOL, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostSteerControl, sendAgentHostCancelControl, foldSteerLiveReceipt, foldInterruptLiveReceipt } from "./runtime/agentHost.js";
85
- import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
75
+ import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
76
+ import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
77
+ import { recordGlobalInterruptResult, recordGlobalSteerResult } from "./message/globalInterrupt.js";
78
+ import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth } from "./observability/runtimeIdentity.js";
79
+ import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
80
+ import { renderAgentConfigurationCatalog } from "./output/agentConfigurationPresentation.js";
81
+ import { parseTaskAgentCapabilityQuery } from "./commands/taskAgentCapabilities.js";
82
+ import { capabilityReaderIdentity } from "./controller/agentCapabilities.js";
83
+ import { formatTimestamp } from "./output/timePresentation.js";
84
+ import { resolveAgentProfileView } from "./profile/agentProfileRuntime.js";
85
+ import { assertCliHomeReleaseFence, describeCliHomeInvocation } from "./release/cliHomeReleaseFence.js";
86
+ import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
87
+ import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
88
+ import { NodeGitWorkspace } from "./repository/gitWorkspace.js";
89
+ import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
86
90
  import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
87
91
  import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
88
92
  import { snapshotWorkItemCandidate } from "./repository/workItemCandidateSnapshot.js";
93
+ import { assessDeltaRecheck } from "./review/deltaRecheck.js";
94
+ import { isCompletedTaskReviewEvidence } from "./review/reviewAcceptance.js";
95
+ import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
96
+ import { resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractResolution.js";
97
+ import { AGENT_HOST_CONTROL_PROTOCOL, foldInterruptLiveReceipt, foldSteerLiveReceipt, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostCancelControl, sendAgentHostSteerControl } from "./runtime/agentHost.js";
98
+ import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
99
+ import { requireManagedTaskCaller, resolveManagedTaskReader } from "./runtime/managedCaller.js";
100
+ import { assertRuntimeCoherence } from "./runtime/runtimeCoherence.js";
101
+ import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
102
+ import { openCurrentTaskStore, validateCurrentTaskStore } from "./storage/currentTaskStore.js";
103
+ import { SqliteSchemaMigrationError } from "./storage/sqliteSchema.js";
89
104
  import { inspectStorageSchema } from "./storage/storageSchema.js";
90
- import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth, resolveStatusIdentityEnabled } from "./observability/runtimeIdentity.js";
91
105
  import { resolveYuiHome } from "./storage/taskStore.js";
92
- import { openCurrentTaskStore, validateCurrentTaskStore } from "./storage/currentTaskStore.js";
106
+ import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
107
+ import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
108
+ import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./task/remoteDeliveryService.js";
93
109
  import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
94
- import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
110
+ import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
95
111
  import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
96
112
  import { TmuxManager } from "./tmux/tmuxManager.js";
97
- import { WorkItemChangeSetManager } from "./workspace/workItemChangeSetManager.js";
98
- import { workspaceProjectEntry } from "./worktree/managedWorkspace.js";
99
- import { parseWebCommandOptions } from "./web/webServer.js";
100
- import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "./executor/agentConfigurationCatalog.js";
101
- import { resolveAgentProfileView } from "./profile/agentProfileRuntime.js";
102
- import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
103
113
  import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
104
- import { SqliteSchemaMigrationError } from "./storage/sqliteSchema.js";
105
- import { assertRuntimeCoherence } from "./runtime/runtimeCoherence.js";
106
- import { requireManagedTaskCaller, resolveManagedTaskReader } from "./runtime/managedCaller.js";
107
- import { operatorOfflineCommand, taskDiagnosticTarget } from "./cli/managedDiagnostics.js";
108
- import { readSessionBootstrapManifest, refreshManagedSessionCliWrappers } from "./context/sessionBootstrapManifest.js";
109
- import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
110
- import { resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractResolution.js";
111
- import { assessDeltaRecheck } from "./review/deltaRecheck.js";
112
- import { isCompletedTaskReviewEvidence } from "./review/reviewAcceptance.js";
113
- import { NodeGitWorkspace } from "./repository/gitWorkspace.js";
114
+ import { parseWebCommandOptions } from "./web/webServer.js";
114
115
  import { currentWorkItemExecutionGroup } from "./workItem/workItem.js";
116
+ import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
117
+ import { WorkItemChangeSetManager } from "./workspace/workItemChangeSetManager.js";
118
+ import { workspaceProjectEntry } from "./worktree/managedWorkspace.js";
115
119
  const VERSION = YUI_VERSION;
116
120
  const taskFinalReviewInvocation = extractTaskFinalReviewRequest(process.argv.slice(2));
117
121
  const rawArgs = [...taskFinalReviewInvocation.args];
@@ -206,6 +210,18 @@ export async function main() {
206
210
  if (args[0] === "version")
207
211
  throw usageError("Version usage: yui version");
208
212
  const { contract: taskFinalReviewContract, verifiedStore } = await preflightManagedTaskControlPlane();
213
+ assertTaskInvocationScope(args, process.env);
214
+ if (["config", "resources"].includes(args[0] ?? "") && (managedInvocation
215
+ || process.env.YUI_ROLE !== undefined || process.env.YUI_AGENT_ID !== undefined
216
+ || process.env.YUI_NATIVE_SESSION_ID !== undefined)) {
217
+ const ownedStore = verifiedStore === undefined ? openCurrentTaskStore(home) : undefined;
218
+ try {
219
+ assertConfigurationAuthority(args, verifiedStore ?? ownedStore, process.env);
220
+ }
221
+ finally {
222
+ ownedStore?.close();
223
+ }
224
+ }
209
225
  if (args[0] === "update") {
210
226
  if (jsonOutput)
211
227
  throw usageError("Update does not support --json.");
@@ -456,38 +472,32 @@ export async function main() {
456
472
  scope: options.scope,
457
473
  environment: process.env
458
474
  });
459
- if (resolveStatusIdentityEnabled(process.env)) {
460
- // Issue 11 read-only identity/metrics section. Every fact is observed;
461
- // missing producers render `unsupported` and storage contradictions
462
- // fail closed with exit code 5.
463
- const cliEntry = fileURLToPath(import.meta.url);
464
- const packageRoot = resolve(cliEntry, "..", "..");
465
- const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
466
- const storage = collectStorageIdentity(home);
467
- const droppedEvents = countDroppedInboxEvents(home);
468
- let runtime;
469
- try {
470
- const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
471
- runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
472
- }
473
- catch {
474
- runtime = { source: "unsupported", droppedEvents };
475
- }
476
- const mismatch = summarizeDurablePhysicalMismatch(snapshot);
477
- const identitySection = renderRuntimeIdentitySection({
478
- build,
479
- storage,
480
- runtime,
481
- mismatch,
482
- inventoryRssBytes: snapshot.summary.rssBytes
483
- });
484
- emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
485
- // The exact current storage contract fails closed on contradictions.
486
- if (evaluateStorageHealth(storage).status === "fail")
487
- process.exitCode = 5;
488
- return;
475
+ // Status always includes identity/health. Missing producers stay
476
+ // `unsupported`; storage contradictions retain the nonzero health exit.
477
+ const cliEntry = fileURLToPath(import.meta.url);
478
+ const packageRoot = resolve(cliEntry, "..", "..");
479
+ const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
480
+ const storage = collectStorageIdentity(home);
481
+ const droppedEvents = countDroppedInboxEvents(home);
482
+ let runtime;
483
+ try {
484
+ const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
485
+ runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
486
+ }
487
+ catch {
488
+ runtime = { source: "unsupported", droppedEvents };
489
489
  }
490
- emit(renderControllerResourceStatus(snapshot, options.verbose), false, snapshot);
490
+ const mismatch = summarizeDurablePhysicalMismatch(snapshot);
491
+ const identitySection = renderRuntimeIdentitySection({
492
+ build,
493
+ storage,
494
+ runtime,
495
+ mismatch,
496
+ inventoryRssBytes: snapshot.summary.rssBytes
497
+ });
498
+ emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
499
+ if (evaluateStorageHealth(storage).status === "fail")
500
+ process.exitCode = 5;
491
501
  return;
492
502
  }
493
503
  if (method === "cleanup") {
@@ -528,7 +538,10 @@ export async function main() {
528
538
  throw usageError("Controller usage: yui controller status [--all] [--verbose] | "
529
539
  + "cleanup [--all] | stop | restart.");
530
540
  }
531
- validateCurrentTaskStore(home);
541
+ // Stopping an exactly identified Controller is a recovery operation:
542
+ // malformed domain records must not prevent quiescing its process.
543
+ if (method === "restart")
544
+ validateCurrentTaskStore(home);
532
545
  const controllerMethod = method;
533
546
  const updateHandoverOwner = process.env.YUI_UPDATE_HANDOVER_OWNER_PID;
534
547
  // A pre-fix updater cannot pass the owner environment variable to the
@@ -580,6 +593,17 @@ export async function main() {
580
593
  emit("Cancelled.");
581
594
  return;
582
595
  }
596
+ assertTaskInvocationScope(resolved, process.env);
597
+ if (resolved[0] === "task" && resolved[1] === "role" && resolved[2] === "capabilities") {
598
+ const query = parseTaskAgentCapabilityQuery(resolved.slice(3));
599
+ const result = await callController(home, "task.role-capabilities", {
600
+ ...query, reader: capabilityReaderIdentity(process.env)
601
+ }, { timeoutMs: 15_000 });
602
+ emit(`Configuration: ${result.context.selection} ${result.context.taskId}/${result.context.roleName}\n`
603
+ + "Discovery uses the Controller's current native account environment; settings files and secret values are not historical snapshots.\n"
604
+ + renderAgentConfigurationCatalog(result), false, result);
605
+ return;
606
+ }
583
607
  const validateAgentConfiguration = await preflightAgentConfigurationMutation(resolved, store, catalogs);
584
608
  const executor = new NodeCommandExecutor();
585
609
  const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), executor, {
@@ -588,1254 +612,1262 @@ export async function main() {
588
612
  terminalInput: process.stdin,
589
613
  onWarning: (message) => process.stderr.write(`Warning: ${message}\n`)
590
614
  });
591
- const schedulerStore = new FileSchedulerStoreAdapter(store, openSchedulerTelemetry(home, store.getConfig()));
592
- const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
593
- const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
594
- const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
595
- environment: process.env,
596
- onError: (error) => {
597
- const message = error instanceof Error ? error.message : String(error);
598
- process.stderr.write(`Controller runtime error: ${message}\n`);
599
- }
600
- });
601
- const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
602
- if (resolved[0] === "web") {
603
- if (managedInvocation || process.env.YUI_ROLE || process.env.YUI_NATIVE_SESSION_ID) {
604
- throw usageError("The Web user ingress must be started from a local user terminal, not a managed Session.");
605
- }
606
- if (resolved.length === 2 && (resolved[1] === "--status" || resolved[1] === "--stop")) {
607
- const status = await callController(home, "web.status", {});
608
- if (resolved[1] === "--status")
609
- emit(status ? `Yui web control room: ${status.url}\n` : "Web is not running.\n", false, status);
610
- else {
611
- if (status)
612
- await callController(home, "web.stop", { id: status.id });
613
- emit("Web listener stopped; Controller and Agents are unchanged.\n");
615
+ const telemetry = openSchedulerTelemetry(home, store.getConfig());
616
+ try {
617
+ const schedulerStore = new FileSchedulerStoreAdapter(store, telemetry);
618
+ const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
619
+ const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
620
+ const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
621
+ environment: process.env,
622
+ onError: (error) => {
623
+ const message = error instanceof Error ? error.message : String(error);
624
+ process.stderr.write(`Controller runtime error: ${message}\n`);
614
625
  }
615
- return;
616
- }
617
- if (jsonOutput)
618
- throw usageError("Web start does not support --json; use --status.");
619
- const options = parseWebCommandOptions(resolved.slice(1));
620
- const id = randomUUID();
621
- await callController(home, "web.start", { ...options, id });
622
- const displayHost = options.host === "::1" ? "[::1]" : options.host;
623
- process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
624
- await new Promise((resolve) => {
625
- const stop = () => {
626
- process.off("SIGINT", stop);
627
- process.off("SIGTERM", stop);
628
- resolve();
629
- };
630
- process.once("SIGINT", stop);
631
- process.once("SIGTERM", stop);
632
626
  });
633
- await callController(home, "web.stop", { id });
634
- return;
635
- }
636
- if (resolved[0] === "config") {
637
- const domain = resolved[1];
638
- const roleOptions = {
639
- yuiHome: home,
640
- env: process.env
641
- };
642
- if (domain === "show") {
643
- const result = runConfigOverview(resolved.slice(2), store, process.env, resolveCliIdentity(process.env), roleOptions);
644
- emit(result.output, false, result.data);
645
- return;
646
- }
647
- if (CONFIG_DOMAINS.includes(domain ?? "")) {
648
- const configDomain = domain;
649
- const domainArgs = resolved.slice(2);
650
- const result = runConfigCommand(configDomain, domainArgs, store);
651
- if (domainArgs[0] === "set"
652
- && domainArgs[1] === "reconciliation-interval-seconds") {
653
- const refresh = await refreshRunningFileTaskControllerConfiguration(home, { environment: process.env });
654
- emit(withControllerRefreshWarning(result.output, refresh, "Controller configuration"));
627
+ const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
628
+ if (resolved[0] === "web") {
629
+ if (managedInvocation || process.env.YUI_ROLE || process.env.YUI_NATIVE_SESSION_ID) {
630
+ throw usageError("The Web user ingress must be started from a local user terminal, not a managed Session.");
631
+ }
632
+ if (resolved.length === 2 && (resolved[1] === "--status" || resolved[1] === "--stop")) {
633
+ const status = await callController(home, "web.status", {});
634
+ if (resolved[1] === "--status")
635
+ emit(status ? `Yui web control room: ${status.url}\n` : "Web is not running.\n", false, status);
636
+ else {
637
+ if (status)
638
+ await callController(home, "web.stop", { id: status.id });
639
+ emit("Web listener stopped; Controller and Agents are unchanged.\n");
640
+ }
655
641
  return;
656
642
  }
657
- emit(result.output, false, result.data);
643
+ if (jsonOutput)
644
+ throw usageError("Web start does not support --json; use --status.");
645
+ const options = parseWebCommandOptions(resolved.slice(1));
646
+ const id = randomUUID();
647
+ await callController(home, "web.start", { ...options, id });
648
+ const displayHost = options.host === "::1" ? "[::1]" : options.host;
649
+ process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
650
+ await new Promise((resolve) => {
651
+ const stop = () => {
652
+ process.off("SIGINT", stop);
653
+ process.off("SIGTERM", stop);
654
+ resolve();
655
+ };
656
+ process.once("SIGINT", stop);
657
+ process.once("SIGTERM", stop);
658
+ });
659
+ await callController(home, "web.stop", { id });
658
660
  return;
659
661
  }
660
- if (domain === "agent") {
661
- const agentArgs = resolved.slice(2);
662
- if (agentArgs[0] === "capabilities") {
663
- if (agentArgs.length !== 2) {
664
- throw usageError("Agent capabilities usage: yui config agent capabilities <agent-id>");
665
- }
666
- const agent = store.getConfiguredAgent(agentArgs[1] ?? "");
667
- if (agent === null)
668
- throw agentNotFound(agentArgs[1] ?? "");
669
- const result = await catalogs.resolve({
670
- agent,
671
- cwd: store.getConfig().defaultWorkspace ?? process.cwd()
672
- });
673
- emit(renderAgentConfigurationCatalog(result), false, result);
662
+ if (resolved[0] === "config") {
663
+ const domain = resolved[1];
664
+ const roleOptions = {
665
+ yuiHome: home,
666
+ env: process.env
667
+ };
668
+ if (domain === "show") {
669
+ const result = runConfigOverview(resolved.slice(2), store, process.env, roleOptions);
670
+ emit(result.output, false, result.data);
671
+ return;
672
+ }
673
+ if (CONFIG_DOMAINS.includes(domain ?? "")) {
674
+ const configDomain = domain;
675
+ const domainArgs = resolved.slice(2);
676
+ const result = runConfigCommand(configDomain, domainArgs, store);
677
+ if (domainArgs[0] === "set"
678
+ && domainArgs[1] === "reconciliation-interval-seconds") {
679
+ const refresh = await refreshRunningFileTaskControllerConfiguration(home, { environment: process.env });
680
+ emit(withControllerRefreshWarning(result.output, refresh, "Controller configuration"));
681
+ return;
682
+ }
683
+ emit(result.output, false, result.data);
674
684
  return;
675
685
  }
676
- const affectedAgentId = agentArgs[1];
677
- const previousAgent = typeof affectedAgentId === "string"
678
- ? store.getConfiguredAgent(affectedAgentId)
679
- : null;
680
- const output = runAgentCommand(agentArgs, store);
681
- if (agentArgs[0] === "add"
682
- || agentArgs[0] === "update"
683
- || agentArgs[0] === "remove") {
684
- const currentAgent = typeof affectedAgentId === "string"
686
+ if (domain === "agent") {
687
+ const agentArgs = resolved.slice(2);
688
+ if (agentArgs[0] === "capabilities") {
689
+ if (agentArgs.length !== 2 && !(agentArgs.length === 3 && agentArgs[2] === "--refresh")) {
690
+ throw usageError("Agent capabilities usage: yui config agent capabilities <agent-id> [--refresh]");
691
+ }
692
+ const agent = store.getConfiguredAgent(agentArgs[1] ?? "");
693
+ if (agent === null)
694
+ throw agentNotFound(agentArgs[1] ?? "");
695
+ const result = await catalogs.resolve({
696
+ agent,
697
+ cwd: store.getConfig().defaultWorkspace ?? process.cwd(),
698
+ refresh: agentArgs[2] === "--refresh"
699
+ });
700
+ emit(renderAgentConfigurationCatalog(result), false, result);
701
+ return;
702
+ }
703
+ const affectedAgentId = agentArgs[1];
704
+ const previousAgent = typeof affectedAgentId === "string"
685
705
  ? store.getConfiguredAgent(affectedAgentId)
686
706
  : null;
687
- const capabilityNotice = currentAgent !== null
688
- && (agentArgs[0] === "add" || agentArgs[0] === "update")
689
- ? renderAgentConfigurationResolutionNotice(await catalogs.resolve({
690
- agent: currentAgent,
691
- cwd: store.getConfig().defaultWorkspace ?? process.cwd()
692
- }))
693
- : "";
694
- const scope = agentEnvironmentRefreshScope(previousAgent, currentAgent, store.listConfiguredAgents());
695
- const refresh = await refreshRunningFileTaskControllerEnvironment(home, store, process.env, scope);
696
- emit(withControllerRefreshWarning(`${output.trimEnd()}${capabilityNotice.length === 0 ? "\n" : `\n${capabilityNotice}`}`, refresh, "Agent environment"));
707
+ const output = runAgentCommand(agentArgs, store);
708
+ if (agentArgs[0] === "add"
709
+ || agentArgs[0] === "update"
710
+ || agentArgs[0] === "remove") {
711
+ const currentAgent = typeof affectedAgentId === "string"
712
+ ? store.getConfiguredAgent(affectedAgentId)
713
+ : null;
714
+ const capabilityNotice = currentAgent !== null
715
+ && (agentArgs[0] === "add" || agentArgs[0] === "update")
716
+ ? renderAgentConfigurationResolutionNotice(await catalogs.resolve({
717
+ agent: currentAgent,
718
+ cwd: store.getConfig().defaultWorkspace ?? process.cwd()
719
+ }))
720
+ : "";
721
+ const scope = agentEnvironmentRefreshScope(previousAgent, currentAgent, store.listConfiguredAgents());
722
+ const refresh = await refreshRunningFileTaskControllerEnvironment(home, store, process.env, scope);
723
+ emit(withControllerRefreshWarning(`${output.trimEnd()}${capabilityNotice.length === 0 ? "\n" : `\n${capabilityNotice}`}`, refresh, "Agent environment"));
724
+ return;
725
+ }
726
+ emit(output);
697
727
  return;
698
728
  }
699
- emit(output);
700
- return;
701
- }
702
- if (domain === "profile") {
703
- const result = runProfileCommand(resolved.slice(2), store, () => new Date(), validateAgentConfiguration === undefined ? {} : { validateAgentConfiguration });
704
- emit(result.output, false, result.data);
705
- return;
706
- }
707
- if (domain === "role") {
708
- const result = runGlobalRoleCommand(resolved.slice(2), store, roleOptions);
709
- if (typeof result !== "string") {
710
- throw new Error("Config Role commands cannot enter a runtime Session.");
729
+ if (domain === "profile") {
730
+ const result = runProfileCommand(resolved.slice(2), store, () => new Date(), validateAgentConfiguration === undefined ? {} : { validateAgentConfiguration });
731
+ emit(result.output, false, result.data);
732
+ return;
711
733
  }
712
- emit(result);
713
- return;
734
+ if (domain === "role") {
735
+ const result = runGlobalRoleCommand(resolved.slice(2), store, roleOptions);
736
+ if (typeof result !== "string") {
737
+ throw new Error("Config Role commands cannot enter a runtime Session.");
738
+ }
739
+ emit(result);
740
+ return;
741
+ }
742
+ throw usageError(`Unknown configuration domain: ${domain ?? ""}.`);
714
743
  }
715
- throw usageError(`Unknown configuration domain: ${domain ?? ""}.`);
716
- }
717
- if (resolved[0] === "project") {
718
- const result = await runProjectCommand(resolved.slice(1), store, { environment: process.env });
719
- emit(result.output, false, result.data);
720
- return;
721
- }
722
- if (resolved[0] === "session") {
723
- if (resolved[1] === "stop") {
724
- const options = parseSessionStopOptions(resolved.slice(2));
725
- const result = await runSessionStopCommand({
726
- options,
727
- runtime: {
728
- beginMaintenance: () => acquireHandoverLock(home),
729
- snapshot: () => ({
730
- candidates: schedulerStore.listRuntimeSessionCandidates(),
731
- dormant: schedulerStore.listDormantRuntimeOwners()
732
- }),
733
- drainController: () => runtime.drainController(),
734
- stopController: () => stopFileTaskController(home, {
735
- environment: process.env
736
- }),
737
- startController: async () => {
738
- await ensureFileTaskController(home, { environment: process.env });
739
- },
740
- stopDormantSession: (candidate) => runtime.stopDormantSession(candidate)
741
- },
742
- environment: process.env
743
- });
744
- process.exitCode = result.exitCode;
744
+ if (resolved[0] === "project") {
745
+ const result = await runProjectCommand(resolved.slice(1), store, { environment: process.env });
745
746
  emit(result.output, false, result.data);
746
747
  return;
747
748
  }
748
- const roleOptions = {
749
- yuiHome: home,
750
- env: process.env,
751
- jsonOutput
752
- };
753
- const sessionArgs = resolved[1] === "enter" || resolved[1] === "context"
754
- ? [resolved[1], ...resolved.slice(2)]
755
- : ["session", resolved[1] ?? "", ...resolved.slice(2)];
756
- const result = runGlobalRoleCommand(sessionArgs, store, roleOptions);
757
- if (typeof result === "string") {
758
- emit(result);
759
- return;
760
- }
761
- // The session surface (record/replace/enter/context) only ever yields the
762
- // enter control; the live input actions live under the top-level `role`
763
- // command below, never here.
764
- if (result.kind !== "enter") {
765
- throw new Error("Session commands cannot perform a live input control.");
766
- }
767
- await ensureFileTaskController(home, { environment: process.env });
768
- await runtime.prepareGlobalRoleEnter(result.role.name);
769
- tmux.attachRole("operator", result.role.name, "auto");
770
- return;
771
- }
772
- if (resolved[0] === "role") {
773
- // decision-3 §7 CLI grammar: the durable Global input actions are top-level
774
- // `role message queue|steer <global-role> …` and `role interrupt
775
- // <global-role> …`, distinct from `config role …` (desired configuration)
776
- // and `session …` (native session lifecycle). Core persists the durable
777
- // Global-owned Message, proves owner/target/capability/writer-fence from
778
- // durable state, and returns either a string disposition (queued,
779
- // idempotent-replay, or an explicit not-steered/not-interrupted failure with
780
- // its exact code) or a resolved live intent. The CLI performs at most one
781
- // native edge with scope "global" and taskId omitted — the same shared
782
- // resolver and transport as a Task Role, never a fabricated Task.
783
- let globalInputFailure;
784
- const roleOptions = {
785
- yuiHome: home,
786
- env: process.env,
787
- jsonOutput,
788
- onInputFailure: failure => { globalInputFailure = failure; }
789
- };
790
- const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
791
- if (resolved[1] === "message" && resolved[2] === "queue" && resolved[3] !== undefined) {
792
- await callController(home, "scheduler.signal", {
793
- key: `global-role:${encodeURIComponent(resolved[3])}`
794
- }).catch(() => { });
795
- }
796
- if (typeof result === "string") {
797
- if (globalInputFailure !== undefined) {
798
- emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
749
+ if (resolved[0] === "session") {
750
+ if (resolved[1] === "stop") {
751
+ const options = parseSessionStopOptions(resolved.slice(2));
752
+ const result = await runSessionStopCommand({
753
+ options,
754
+ runtime: {
755
+ beginMaintenance: () => acquireHandoverLock(home),
756
+ snapshot: () => ({
757
+ candidates: schedulerStore.listRuntimeSessionCandidates(),
758
+ dormant: schedulerStore.listDormantRuntimeOwners()
759
+ }),
760
+ drainController: () => runtime.drainController(),
761
+ stopController: () => stopFileTaskController(home, {
762
+ environment: process.env
763
+ }),
764
+ startController: async () => {
765
+ await ensureFileTaskController(home, { environment: process.env });
766
+ },
767
+ stopDormantSession: (candidate) => runtime.stopDormantSession(candidate)
768
+ },
769
+ environment: process.env
770
+ });
771
+ process.exitCode = result.exitCode;
772
+ emit(result.output, false, result.data);
799
773
  return;
800
774
  }
801
- emit(result);
775
+ const roleOptions = {
776
+ yuiHome: home,
777
+ env: process.env,
778
+ jsonOutput
779
+ };
780
+ const sessionArgs = resolved[1] === "enter" || resolved[1] === "context"
781
+ ? [resolved[1], ...resolved.slice(2)]
782
+ : ["session", resolved[1] ?? "", ...resolved.slice(2)];
783
+ const result = runGlobalRoleCommand(sessionArgs, store, roleOptions);
784
+ if (typeof result === "string") {
785
+ emit(result);
786
+ return;
787
+ }
788
+ // The session surface (record/replace/enter/context) only ever yields the
789
+ // enter control; the live input actions live under the top-level `role`
790
+ // command below, never here.
791
+ if (result.kind !== "enter") {
792
+ throw new Error("Session commands cannot perform a live input control.");
793
+ }
794
+ await ensureFileTaskController(home, { environment: process.env });
795
+ await runtime.prepareGlobalRoleEnter(result.role.name);
796
+ tmux.attachRole("operator", result.role.name, "auto");
802
797
  return;
803
798
  }
804
- if (result.kind === "input-steer") {
805
- // Core already persisted the durable Global Message and proved target +
806
- // capability + writer fence from the Global Role's own Session set. This
807
- // is the single live edge: one native steer of the exact current Turn,
808
- // scope "global", no retarget and no fallback to interrupt or queue.
809
- await ensureFileTaskController(home, { environment: process.env });
810
- let control;
811
- try {
812
- control = await sendAgentHostSteerControl({
813
- home,
814
- scope: "global",
799
+ if (resolved[0] === "role") {
800
+ // decision-3 §7 CLI grammar: the durable Global input actions are top-level
801
+ // `role message queue|steer <global-role> …` and `role interrupt
802
+ // <global-role> …`, distinct from `config role …` (desired configuration)
803
+ // and `session …` (native session lifecycle). Core persists the durable
804
+ // Global-owned Message, proves owner/target/capability/writer-fence from
805
+ // durable state, and returns either a string disposition (queued,
806
+ // idempotent-replay, or an explicit not-steered/not-interrupted failure with
807
+ // its exact code) or a resolved live intent. The CLI performs at most one
808
+ // native edge with scope "global" and taskId omitted — the same shared
809
+ // resolver and transport as a Task Role, never a fabricated Task.
810
+ let globalInputFailure;
811
+ const roleOptions = {
812
+ yuiHome: home,
813
+ env: process.env,
814
+ jsonOutput,
815
+ onInputFailure: failure => { globalInputFailure = failure; }
816
+ };
817
+ const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
818
+ if (resolved[1] === "message" && resolved[2] === "queue" && resolved[3] !== undefined) {
819
+ await callController(home, "scheduler.signal", {
820
+ key: `global-role:${encodeURIComponent(resolved[3])}`
821
+ }).catch(() => { });
822
+ }
823
+ if (typeof result === "string") {
824
+ if (globalInputFailure !== undefined) {
825
+ emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
826
+ return;
827
+ }
828
+ emit(result);
829
+ return;
830
+ }
831
+ if (result.kind === "input-steer") {
832
+ // Core already persisted the durable Global Message and proved target +
833
+ // capability + writer fence from the Global Role's own Session set. This
834
+ // is the single live edge: one native steer of the exact current Turn,
835
+ // scope "global", no retarget and no fallback to interrupt or queue.
836
+ await ensureFileTaskController(home, { environment: process.env });
837
+ let control;
838
+ try {
839
+ control = await sendAgentHostSteerControl({
840
+ home,
841
+ scope: "global",
842
+ roleName: result.roleName,
843
+ control: {
844
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
845
+ type: "steer-turn",
846
+ nativeSessionId: result.target.nativeSessionId,
847
+ nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
848
+ authority: result.target.authority,
849
+ run: { attemptId: result.receiptId, boundedText: result.text }
850
+ }
851
+ });
852
+ }
853
+ catch (error) {
854
+ recordGlobalSteerResult(store, result.roleName, result.messageId, {
855
+ state: "steer-unknown", outcome: "pending",
856
+ detail: error instanceof Error ? error.message : String(error)
857
+ });
858
+ throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
859
+ + `${error instanceof Error ? error.message : String(error)}. `
860
+ + "The Message is retained and its outcome is recorded from the Host; whether the "
861
+ + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
862
+ + "do not reissue the same input under a new requestId or a different action.");
863
+ }
864
+ const steer = foldSteerLiveReceipt(control);
865
+ recordGlobalSteerResult(store, result.roleName, result.messageId, steer);
866
+ if (steer.state !== "steered") {
867
+ emitControlFailure(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_NOT_DELIVERED", { roleName: result.roleName, messageId: result.messageId, steer });
868
+ return;
869
+ }
870
+ emit(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), false, {
815
871
  roleName: result.roleName,
816
- control: {
817
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
818
- type: "steer-turn",
819
- nativeSessionId: result.target.nativeSessionId,
820
- nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
821
- authority: result.target.authority,
822
- run: { attemptId: result.receiptId, boundedText: result.text }
823
- }
872
+ messageId: result.messageId, steer
824
873
  });
874
+ return;
825
875
  }
826
- catch (error) {
827
- recordGlobalSteerResult(store, result.roleName, result.messageId, {
828
- state: "steer-unknown", outcome: "pending",
829
- detail: error instanceof Error ? error.message : String(error)
876
+ if (result.kind === "input-interrupt") {
877
+ // The single live edge for a Global interrupt: one native cancel of the
878
+ // exact current Turn, scope "global". Never a kill/restart/detach. Any
879
+ // then-handoff was already claimed durably by Core (an existing Global
880
+ // Message owned by this Role) and is delivered once by the ordinary
881
+ // continuation path after this Turn reaches a proven terminal.
882
+ await ensureFileTaskController(home, { environment: process.env });
883
+ let control;
884
+ try {
885
+ control = await sendAgentHostCancelControl({
886
+ home,
887
+ scope: "global",
888
+ roleName: result.roleName,
889
+ control: {
890
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
891
+ type: "cancel",
892
+ nativeOnly: true,
893
+ nativeSessionId: result.target.nativeSessionId,
894
+ attemptId: result.target.attemptId,
895
+ authority: result.target.authority
896
+ }
897
+ });
898
+ }
899
+ catch (error) {
900
+ recordGlobalInterruptResult(store, result.roleName, result.receiptId, {
901
+ state: "interrupt-unknown", outcome: "cancel-requested",
902
+ detail: error instanceof Error ? error.message : String(error)
903
+ });
904
+ throw runtimeError(`Interrupt of global role ${result.roleName} did not complete: `
905
+ + `${error instanceof Error ? error.message : String(error)}. `
906
+ + "No process was killed; re-read the Session before retrying.");
907
+ }
908
+ const interrupt = foldInterruptLiveReceipt(control);
909
+ recordGlobalInterruptResult(store, result.roleName, result.receiptId, interrupt);
910
+ if (interrupt.state !== "interrupt-requested") {
911
+ emitControlFailure(interruptReceiptOutput(result.output, result.roleName, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN" : "INTERRUPT_NOT_DELIVERED", { roleName: result.roleName, interrupt });
912
+ return;
913
+ }
914
+ emit(interruptReceiptOutput(result.output, result.roleName, interrupt), false, {
915
+ roleName: result.roleName,
916
+ ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
917
+ interrupt
830
918
  });
831
- throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
832
- + `${error instanceof Error ? error.message : String(error)}. `
833
- + "The Message is retained and its outcome is recorded from the Host; whether the "
834
- + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
835
- + "do not reissue the same input under a new requestId or a different action.");
836
- }
837
- const steer = foldSteerLiveReceipt(control);
838
- recordGlobalSteerResult(store, result.roleName, result.messageId, steer);
839
- if (steer.state !== "steered") {
840
- emitControlFailure(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_NOT_DELIVERED", { roleName: result.roleName, messageId: result.messageId, steer });
841
919
  return;
842
920
  }
843
- emit(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), false, {
844
- roleName: result.roleName,
845
- messageId: result.messageId, steer
846
- });
847
- return;
921
+ if (result.kind !== "enter") {
922
+ throw new Error("Role command returned an invalid control result.");
923
+ }
924
+ // A top-level `role` command never enters a runtime Session; that is
925
+ // `session enter`.
926
+ throw usageError("Use 'yui session enter <role>' to attach to a Global Role.");
848
927
  }
849
- if (result.kind === "input-interrupt") {
850
- // The single live edge for a Global interrupt: one native cancel of the
851
- // exact current Turn, scope "global". Never a kill/restart/detach. Any
852
- // then-handoff was already claimed durably by Core (an existing Global
853
- // Message owned by this Role) and is delivered once by the ordinary
854
- // continuation path after this Turn reaches a proven terminal.
855
- await ensureFileTaskController(home, { environment: process.env });
856
- let control;
857
- try {
858
- control = await sendAgentHostCancelControl({
859
- home,
860
- scope: "global",
861
- roleName: result.roleName,
862
- control: {
863
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
864
- type: "cancel",
865
- nativeOnly: true,
866
- nativeSessionId: result.target.nativeSessionId,
867
- attemptId: result.target.attemptId,
868
- authority: result.target.authority
869
- }
870
- });
928
+ if (resolved[0] === "operator") {
929
+ if (resolved[1] === "enter") {
930
+ if (resolved.length !== 2)
931
+ throw usageError("Operator enter usage: yui operator enter.");
932
+ await ensureFileTaskController(home, { environment: process.env });
933
+ await runtime.prepareGlobalRoleEnter("operator");
934
+ tmux.attachRole("operator", "operator", "auto");
935
+ return;
871
936
  }
872
- catch (error) {
873
- recordGlobalInterruptResult(store, result.roleName, result.receiptId, {
874
- state: "interrupt-unknown", outcome: "cancel-requested",
875
- detail: error instanceof Error ? error.message : String(error)
876
- });
877
- throw runtimeError(`Interrupt of global role ${result.roleName} did not complete: `
878
- + `${error instanceof Error ? error.message : String(error)}. `
879
- + "No process was killed; re-read the Session before retrying.");
880
- }
881
- const interrupt = foldInterruptLiveReceipt(control);
882
- recordGlobalInterruptResult(store, result.roleName, result.receiptId, interrupt);
883
- if (interrupt.state !== "interrupt-requested") {
884
- emitControlFailure(interruptReceiptOutput(result.output, result.roleName, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN" : "INTERRUPT_NOT_DELIVERED", { roleName: result.roleName, interrupt });
937
+ const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
938
+ if (result.kind === "output") {
939
+ emit(result.output, false, result.data);
885
940
  return;
886
941
  }
887
- emit(interruptReceiptOutput(result.output, result.roleName, interrupt), false, {
888
- roleName: result.roleName,
889
- ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
890
- interrupt
891
- });
892
- return;
893
- }
894
- if (result.kind !== "enter") {
895
- throw new Error("Role command returned an invalid control result.");
896
- }
897
- // A top-level `role` command never enters a runtime Session; that is
898
- // `session enter`.
899
- throw usageError("Use 'yui session enter <role>' to attach to a Global Role.");
900
- }
901
- if (resolved[0] === "operator") {
902
- if (resolved[1] === "enter") {
903
- if (resolved.length !== 2)
904
- throw usageError("Operator enter usage: yui operator enter.");
905
- await ensureFileTaskController(home, { environment: process.env });
906
- await runtime.prepareGlobalRoleEnter("operator");
907
- tmux.attachRole("operator", "operator", "auto");
908
- return;
909
- }
910
- const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
911
- if (result.kind === "output") {
912
- emit(result.output, false, result.data);
913
- return;
914
- }
915
- if (result.kind !== "session") {
916
- throw new Error("Operator command returned an invalid control result.");
917
- }
918
- await executeOperatorSessionControl(result, home, store, runtime, tmux, catalogs);
919
- return;
920
- }
921
- if (resolved[0] === "task") {
922
- if (resolved[1] === "archive-preflight") {
923
- const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
924
- if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
925
- throw usageError("Archive inspection must remain within this Session's Task.");
942
+ if (result.kind !== "session") {
943
+ throw new Error("Operator command returned an invalid control result.");
926
944
  }
927
- const data = await inspectTaskArchive(workspaceCoordinator, request);
928
- emit(renderTaskArchivePreflight(data), false, data);
945
+ await executeOperatorSessionControl(result, home, store, runtime, tmux, catalogs);
929
946
  return;
930
947
  }
931
- if (resolved[1] === "artifact") {
932
- // File/directory artifacts live in the Task's local Git repository, so
933
- // their save/read/list are asynchronous and handled here rather than in
934
- // the synchronous runTaskCommand chain. Save commits exactly one path and
935
- // returns a self-certifying commit-pinned reference; read pins to a commit
936
- // for frozen evidence; list is an ordinary current read.
937
- const action = resolved[2];
938
- const taskId = resolved[3];
939
- const usage = "Usage: yui task artifact list <task> | read <task> <relative-path> [<commit>] | "
940
- + "save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]";
941
- if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
942
- throw usageError(usage);
943
- }
944
- if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
945
- throw usageError("Artifact is outside the managed Task scope.");
946
- }
947
- if (store.getTask(taskId) === null)
948
- throw usageError(`Task not found: ${taskId}.`);
949
- let data;
950
- if (action === "list") {
951
- if (resolved.length !== 4)
952
- throw usageError(usage);
953
- data = await listArtifactsCapability(home, taskId);
948
+ if (resolved[0] === "task") {
949
+ if (resolved[1] === "archive-preflight") {
950
+ const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
951
+ if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
952
+ throw usageError("Archive inspection must remain within this Session's Task.");
953
+ }
954
+ const data = await inspectTaskArchive(workspaceCoordinator, request);
955
+ emit(renderTaskArchivePreflight(data), false, data);
956
+ return;
954
957
  }
955
- else if (action === "read") {
956
- const relativePath = resolved[4];
957
- const commit = resolved[5];
958
- if (relativePath === undefined || resolved.length > 6)
958
+ if (resolved[1] === "artifact") {
959
+ // File/directory artifacts live in the Task's local Git repository, so
960
+ // their save/read/list are asynchronous and handled here rather than in
961
+ // the synchronous runTaskCommand chain. Save commits exactly one path and
962
+ // returns a self-certifying commit-pinned reference; read pins to a commit
963
+ // for frozen evidence; list is an ordinary current read.
964
+ const action = resolved[2];
965
+ const taskId = resolved[3];
966
+ const usage = "Usage: yui task artifact list <task> | read <task> <relative-path> [<commit>] | "
967
+ + "save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]";
968
+ if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
959
969
  throw usageError(usage);
960
- data = await readArtifactCapability(home, taskId, {
961
- relativePath, ...(commit === undefined ? {} : { commit })
962
- });
970
+ }
971
+ if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
972
+ throw usageError("Artifact is outside the managed Task scope.");
973
+ }
974
+ if (store.getTask(taskId) === null)
975
+ throw usageError(`Task not found: ${taskId}.`);
976
+ let data;
977
+ if (action === "list") {
978
+ if (resolved.length !== 4)
979
+ throw usageError(usage);
980
+ data = await listArtifactsCapability(home, taskId);
981
+ }
982
+ else if (action === "read") {
983
+ const relativePath = resolved[4];
984
+ const commit = resolved[5];
985
+ if (relativePath === undefined || resolved.length > 6)
986
+ throw usageError(usage);
987
+ data = await readArtifactCapability(home, taskId, {
988
+ relativePath, ...(commit === undefined ? {} : { commit })
989
+ });
990
+ }
991
+ else {
992
+ // save: a delivery-authoritative action; a managed Task caller must be the current Leader.
993
+ taskLocalActor(store, process.env, taskId);
994
+ const relativePath = resolved[4];
995
+ const content = resolved[5];
996
+ if (relativePath === undefined || content === undefined)
997
+ throw usageError(usage);
998
+ const rest = resolved.slice(6);
999
+ let message;
1000
+ let expectedHead;
1001
+ for (let index = 0; index < rest.length; index += 1) {
1002
+ const value = rest[index + 1];
1003
+ if (rest[index] === "--message" && value !== undefined) {
1004
+ message = value;
1005
+ index += 1;
1006
+ continue;
1007
+ }
1008
+ if (rest[index] === "--expected-head" && value !== undefined) {
1009
+ expectedHead = value;
1010
+ index += 1;
1011
+ continue;
1012
+ }
1013
+ throw usageError(usage);
1014
+ }
1015
+ data = await saveArtifactCapability(home, taskId, {
1016
+ relativePath, content,
1017
+ ...(message === undefined ? {} : { message }),
1018
+ ...(expectedHead === undefined ? {} : { expectedHead })
1019
+ });
1020
+ }
1021
+ emit(JSON.stringify(data, null, 2), false, data);
1022
+ return;
963
1023
  }
964
- else {
965
- // save: a delivery-authoritative action; a managed Task caller must be the current Leader.
966
- taskLocalActor(store, process.env, taskId);
967
- const relativePath = resolved[4];
968
- const content = resolved[5];
969
- if (relativePath === undefined || content === undefined)
970
- throw usageError(usage);
971
- const rest = resolved.slice(6);
972
- let message;
973
- let expectedHead;
974
- for (let index = 0; index < rest.length; index += 1) {
975
- const value = rest[index + 1];
976
- if (rest[index] === "--message" && value !== undefined) {
977
- message = value;
978
- index += 1;
979
- continue;
1024
+ if (resolved[1] === "execution") {
1025
+ if (resolved[2] === "stop") {
1026
+ const request = parseTaskExecutionStopRequest(resolved.slice(3));
1027
+ const result = stopTaskExecutionCommand(request, store, { environment: process.env });
1028
+ try {
1029
+ await ensureFileTaskController(home, { environment: process.env });
1030
+ await runtime.stopTaskDurableJobs(result.taskId);
1031
+ await runtime.stopTaskRoleSessions(result.taskId, result.roleNames);
1032
+ await runtime.assertTaskPhysicalResourcesReleased(result.taskId);
1033
+ finalizeStoppedTaskExecution(result.taskId, store);
980
1034
  }
981
- if (rest[index] === "--expected-head" && value !== undefined) {
982
- expectedHead = value;
983
- index += 1;
984
- continue;
1035
+ catch (error) {
1036
+ const message = error instanceof Error ? error.message : String(error);
1037
+ throw runtimeError(`Task execution is stopped and durable progress is preserved, but physical runtime cleanup failed: ${message}`);
985
1038
  }
986
- throw usageError(usage);
1039
+ emit(result.output, false, result);
1040
+ return;
1041
+ }
1042
+ if (resolved[2] === "start") {
1043
+ const taskId = parseTaskExecutionStartRequest(resolved.slice(3));
1044
+ const task = store.getTask(taskId);
1045
+ if (task === null)
1046
+ throw usageError(`Task not found: ${taskId}.`);
1047
+ // Reject managed Task callers before inspecting or starting runtime resources.
1048
+ if (taskLocalActor(store, process.env, taskId) === "leader") {
1049
+ throw usageError("Task execution stop/start requires the global Operator or a human user.");
1050
+ }
1051
+ if (task.executionGate.state === "stopped") {
1052
+ await runtime.assertTaskPhysicalResourcesReleased(taskId);
1053
+ }
1054
+ await ensureFileTaskController(home, { environment: process.env });
1055
+ const result = startTaskExecutionCommand(taskId, store, { environment: process.env });
1056
+ // Idempotent start is also a reliable kick: if an earlier caller
1057
+ // committed the gate but lost its Controller acknowledgement, retrying
1058
+ // start re-signals the same durable wake without creating another one.
1059
+ await runtime.notifyMailboxChanged({ kind: "role", taskId, roleName: "leader" });
1060
+ emit(result.output, false, result);
1061
+ return;
987
1062
  }
988
- data = await saveArtifactCapability(home, taskId, {
989
- relativePath, content,
990
- ...(message === undefined ? {} : { message }),
991
- ...(expectedHead === undefined ? {} : { expectedHead })
1063
+ throw usageError("Task execution usage: yui task execution <stop|start> ...");
1064
+ }
1065
+ if (resolved[1] === "integration") {
1066
+ const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
1067
+ environment: process.env,
1068
+ jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
992
1069
  });
1070
+ emit(result.output, false, result.data);
1071
+ return;
993
1072
  }
994
- emit(JSON.stringify(data, null, 2), false, data);
995
- return;
996
- }
997
- if (resolved[1] === "execution") {
998
- if (resolved[2] === "stop") {
999
- const request = parseTaskExecutionStopRequest(resolved.slice(3));
1000
- const result = stopTaskExecutionCommand(request, store, { environment: process.env });
1001
- try {
1002
- await ensureFileTaskController(home, { environment: process.env });
1003
- await runtime.stopTaskDurableJobs(result.taskId);
1004
- await runtime.stopTaskRoleSessions(result.taskId, result.roleNames);
1005
- await runtime.assertTaskPhysicalResourcesReleased(result.taskId);
1006
- finalizeStoppedTaskExecution(result.taskId, store);
1073
+ if (resolved[1] === "change-set") {
1074
+ const result = await runTaskChangeSetCommand(resolved.slice(2), store);
1075
+ emit(result.output, false, result.data);
1076
+ return;
1077
+ }
1078
+ if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
1079
+ const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
1080
+ environment: process.env
1081
+ });
1082
+ emit(result.output, false, result.data);
1083
+ return;
1084
+ }
1085
+ if (resolved[1] === "publication" && resolved[2] === "verify") {
1086
+ const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
1087
+ verifiers: {
1088
+ github: createGitHubCliPublicationVerifier({
1089
+ environmentPath: process.env.PATH
1090
+ }),
1091
+ gitlab: createGitLabCliPublicationVerifier({
1092
+ environmentPath: process.env.PATH
1093
+ })
1094
+ },
1095
+ environment: process.env
1096
+ });
1097
+ emit(result.output, false, result.data);
1098
+ return;
1099
+ }
1100
+ if (resolved[1] === "overlap") {
1101
+ const result = await runTaskOverlapCommand(resolved.slice(2), store);
1102
+ emit(result.output, false, result.data);
1103
+ return;
1104
+ }
1105
+ if (resolved[1] === "workflow"
1106
+ && (resolved[2] === "run" || resolved[2] === "resume")) {
1107
+ const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
1108
+ environment: process.env,
1109
+ yuiHome: home,
1110
+ ports: createReleaseWorkflowPorts({
1111
+ home,
1112
+ updatePorts: createUpdatePorts(process.env),
1113
+ projectStore: store
1114
+ })
1115
+ });
1116
+ if (result.kind !== "output") {
1117
+ throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
1007
1118
  }
1008
- catch (error) {
1009
- const message = error instanceof Error ? error.message : String(error);
1010
- throw runtimeError(`Task execution is stopped and durable progress is preserved, but physical runtime cleanup failed: ${message}`);
1119
+ emit(result.output, false, result.data);
1120
+ return;
1121
+ }
1122
+ if (resolved[1] === "replace") {
1123
+ const result = await runTaskWorkspaceCommand(resolved.slice(1), store, workspacePreparer);
1124
+ emit(result.output, false, result.data);
1125
+ return;
1126
+ }
1127
+ if (resolved[1] === "work" && resolved[2] === "isolate") {
1128
+ const workItemId = resolved[3];
1129
+ if (workItemId === undefined || resolved.length !== 4) {
1130
+ throw usageError("Task work isolate usage: yui task work isolate <task>/<work>.");
1011
1131
  }
1012
- emit(result.output, false, result);
1132
+ const reference = cliWorkItemReference(workItemId, process.env);
1133
+ const workspace = await workspaceCoordinator.isolateWorkItem(reference.taskId, reference.localId);
1134
+ emit(`Created WorkItem workspace for ${reference.taskId}/${reference.localId}\nWorkspace: ${workspace.root}\n`, false, { workItemRef: reference, workspace });
1013
1135
  return;
1014
1136
  }
1015
- if (resolved[2] === "start") {
1016
- const taskId = parseTaskExecutionStartRequest(resolved.slice(3));
1017
- const task = store.getTask(taskId);
1018
- if (task === null)
1019
- throw usageError(`Task not found: ${taskId}.`);
1020
- // Reject managed Task callers before inspecting or starting runtime resources.
1021
- if (taskLocalActor(store, process.env, taskId) === "leader") {
1022
- throw usageError("Task execution stop/start requires the global Operator or a human user.");
1137
+ if (resolved[1] === "work" && resolved[2] === "review"
1138
+ && resolved[3] === "cleanup") {
1139
+ const reviewRoundId = resolved[4];
1140
+ if (reviewRoundId === undefined || resolved.length !== 5) {
1141
+ throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1023
1142
  }
1024
- if (task.executionGate.state === "stopped") {
1025
- await runtime.assertTaskPhysicalResourcesReleased(taskId);
1143
+ const reference = cliTaskRecordReference(reviewRoundId, "reviewRound", process.env);
1144
+ const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1145
+ if (removal === "dirty") {
1146
+ throw usageError(`ReviewRound workspace is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1026
1147
  }
1027
- await ensureFileTaskController(home, { environment: process.env });
1028
- const result = startTaskExecutionCommand(taskId, store, { environment: process.env });
1029
- // Idempotent start is also a reliable kick: if an earlier caller
1030
- // committed the gate but lost its Controller acknowledgement, retrying
1031
- // start re-signals the same durable wake without creating another one.
1032
- await runtime.notifyMailboxChanged?.({ kind: "role", taskId, roleName: "leader" });
1033
- emit(result.output, false, result);
1148
+ emit(`Cleaned ReviewRound workspace ${reference.taskId}/${reference.localId} (${removal})\n`, false, { reviewRoundRef: reference, workspace: { removal } });
1034
1149
  return;
1035
1150
  }
1036
- throw usageError("Task execution usage: yui task execution <stop|start> ...");
1037
- }
1038
- if (resolved[1] === "integration") {
1039
- const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
1040
- environment: process.env,
1041
- jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
1042
- });
1043
- emit(result.output, false, result.data);
1044
- return;
1045
- }
1046
- if (resolved[1] === "change-set") {
1047
- const result = await runTaskChangeSetCommand(resolved.slice(2), store);
1048
- emit(result.output, false, result.data);
1049
- return;
1050
- }
1051
- if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
1052
- const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
1053
- environment: process.env
1054
- });
1055
- emit(result.output, false, result.data);
1056
- return;
1057
- }
1058
- if (resolved[1] === "publication" && resolved[2] === "verify") {
1059
- const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
1060
- verifiers: {
1061
- github: createGitHubCliPublicationVerifier({
1062
- environmentPath: process.env.PATH
1063
- }),
1064
- gitlab: createGitLabCliPublicationVerifier({
1065
- environmentPath: process.env.PATH
1066
- })
1067
- },
1068
- environment: process.env
1069
- });
1070
- emit(result.output, false, result.data);
1071
- return;
1072
- }
1073
- if (resolved[1] === "overlap") {
1074
- const result = await runTaskOverlapCommand(resolved.slice(2), store);
1075
- emit(result.output, false, result.data);
1076
- return;
1077
- }
1078
- if (resolved[1] === "workflow"
1079
- && (resolved[2] === "run" || resolved[2] === "resume")) {
1080
- const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
1081
- environment: process.env,
1082
- yuiHome: home,
1083
- ports: createReleaseWorkflowPorts({
1084
- home,
1085
- updatePorts: createUpdatePorts(process.env),
1086
- projectStore: store
1087
- })
1088
- });
1089
- if (result.kind !== "output") {
1090
- throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
1151
+ if (resolved[1] === "work" && resolved[2] === "capture") {
1152
+ const workItemId = resolved[3];
1153
+ if (workItemId === undefined || resolved.length !== 4) {
1154
+ throw usageError("Task work capture usage: yui task work capture <task>/<work>.");
1155
+ }
1156
+ const reference = cliWorkItemReference(workItemId, process.env);
1157
+ assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1158
+ const changeSets = await new WorkItemChangeSetManager(store).capture(reference.taskId, reference.localId, taskFinalReviewContract === undefined
1159
+ ? {}
1160
+ : { taskFinalReviewContract });
1161
+ const qualified = `${reference.taskId}/${reference.localId}`;
1162
+ emit(changeSets.length === 0
1163
+ ? `WorkItem workspace has no changes to capture: ${qualified}\n`
1164
+ : `Captured ChangeSets ${changeSets.map(({ id }) => id).join(", ")} from ${qualified}\n`, false, { workItemRef: reference, changeSets });
1165
+ return;
1091
1166
  }
1092
- emit(result.output, false, result.data);
1093
- return;
1094
- }
1095
- if (resolved[1] === "replace") {
1096
- const result = await runTaskWorkspaceCommand(resolved.slice(1), store, workspacePreparer);
1097
- emit(result.output, false, result.data);
1098
- return;
1099
- }
1100
- if (resolved[1] === "work" && resolved[2] === "isolate") {
1101
- const workItemId = resolved[3];
1102
- if (workItemId === undefined || resolved.length !== 4) {
1103
- throw usageError("Task work isolate usage: yui task work isolate <task>/<work>.");
1104
- }
1105
- const reference = cliWorkItemReference(workItemId, process.env);
1106
- const workspace = await workspaceCoordinator.isolateWorkItem(reference.taskId, reference.localId);
1107
- emit(`Created WorkItem workspace for ${reference.taskId}/${reference.localId}\nWorkspace: ${workspace.root}\n`, false, { workItemRef: reference, workspace });
1108
- return;
1109
- }
1110
- if (resolved[1] === "work" && resolved[2] === "review"
1111
- && resolved[3] === "cleanup") {
1112
- const reviewRoundId = resolved[4];
1113
- if (reviewRoundId === undefined || resolved.length !== 5) {
1114
- throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1115
- }
1116
- const reference = cliTaskRecordReference(reviewRoundId, "reviewRound", process.env);
1117
- const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1118
- if (removal === "dirty") {
1119
- throw usageError(`ReviewRound workspace is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1120
- }
1121
- emit(`Cleaned ReviewRound workspace ${reference.taskId}/${reference.localId} (${removal})\n`, false, { reviewRoundRef: reference, workspace: { removal } });
1122
- return;
1123
- }
1124
- if (resolved[1] === "work" && resolved[2] === "capture") {
1125
- const workItemId = resolved[3];
1126
- if (workItemId === undefined || resolved.length !== 4) {
1127
- throw usageError("Task work capture usage: yui task work capture <task>/<work>.");
1128
- }
1129
- const reference = cliWorkItemReference(workItemId, process.env);
1130
- assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1131
- const changeSets = await new WorkItemChangeSetManager(store).capture(reference.taskId, reference.localId, taskFinalReviewContract === undefined
1132
- ? {}
1133
- : { taskFinalReviewContract });
1134
- const qualified = `${reference.taskId}/${reference.localId}`;
1135
- emit(changeSets.length === 0
1136
- ? `WorkItem workspace has no changes to capture: ${qualified}\n`
1137
- : `Captured ChangeSets ${changeSets.map(({ id }) => id).join(", ")} from ${qualified}\n`, false, { workItemRef: reference, changeSets });
1138
- return;
1139
- }
1140
- if (resolved[1] === "work" && resolved[2] === "cleanup") {
1141
- const workItemId = resolved[3];
1142
- const disposition = resolved[4];
1143
- if (workItemId === undefined
1144
- || !["--runtime-only", "--integrated", "--abandon"].includes(disposition ?? "")
1145
- || resolved.length !== 5) {
1146
- throw usageError("Task work cleanup usage: yui task work cleanup <task>/<work> "
1147
- + "(--runtime-only|--integrated|--abandon).");
1148
- }
1149
- const reference = cliWorkItemReference(workItemId, process.env);
1150
- const qualified = `${reference.taskId}/${reference.localId}`;
1151
- assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1152
- if (disposition === "--runtime-only") {
1153
- let runtimeCleanup;
1167
+ if (resolved[1] === "work" && resolved[2] === "cleanup") {
1168
+ const workItemId = resolved[3];
1169
+ const disposition = resolved[4];
1170
+ if (workItemId === undefined
1171
+ || !["--runtime-only", "--integrated", "--abandon"].includes(disposition ?? "")
1172
+ || resolved.length !== 5) {
1173
+ throw usageError("Task work cleanup usage: yui task work cleanup <task>/<work> "
1174
+ + "(--runtime-only|--integrated|--abandon).");
1175
+ }
1176
+ const reference = cliWorkItemReference(workItemId, process.env);
1177
+ const qualified = `${reference.taskId}/${reference.localId}`;
1178
+ assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1179
+ if (disposition === "--runtime-only") {
1180
+ let runtimeCleanup;
1181
+ try {
1182
+ runtimeCleanup = await workspaceCoordinator.cleanupWorkItemRuntime(reference.taskId, reference.localId);
1183
+ }
1184
+ catch (error) {
1185
+ throw cleanupCliError(error, `work-item:${qualified}`);
1186
+ }
1187
+ emit(`Released WorkItem runtime ${qualified}; retained its Session and worktree\n`, false, {
1188
+ workItem: store.getWorkItem(reference.taskId, reference.localId),
1189
+ runtime: { cleanup: runtimeCleanup },
1190
+ worktree: { retained: true }
1191
+ });
1192
+ return;
1193
+ }
1194
+ const cleanedAs = disposition === "--integrated" ? "integrated" : "abandoned";
1195
+ if (cleanedAs === "integrated") {
1196
+ try {
1197
+ await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
1198
+ }
1199
+ catch (error) {
1200
+ throw cleanupCliError(error, `work-item:${qualified}`);
1201
+ }
1202
+ }
1203
+ let removal;
1154
1204
  try {
1155
- runtimeCleanup = await workspaceCoordinator.cleanupWorkItemRuntime(reference.taskId, reference.localId);
1205
+ removal = await workspaceCoordinator.cleanupWorkItem(reference.taskId, reference.localId, cleanedAs);
1156
1206
  }
1157
1207
  catch (error) {
1158
1208
  throw cleanupCliError(error, `work-item:${qualified}`);
1159
1209
  }
1160
- emit(`Released WorkItem runtime ${qualified}; retained its Session and worktree\n`, false, {
1210
+ if (removal === "dirty") {
1211
+ throw usageError(`WorkItem worktree is dirty and was retained: ${qualified}.`, undefined, cleanupBlockedDetails("dirty-worktree", `work-item:${qualified}`, true));
1212
+ }
1213
+ emit(`Cleaned WorkItem worktree ${qualified} (${cleanedAs})\n`, false, {
1161
1214
  workItem: store.getWorkItem(reference.taskId, reference.localId),
1162
- runtime: { cleanup: runtimeCleanup },
1163
- worktree: { retained: true }
1215
+ worktree: { removal, disposition: cleanedAs }
1164
1216
  });
1165
1217
  return;
1166
1218
  }
1167
- const cleanedAs = disposition === "--integrated" ? "integrated" : "abandoned";
1168
- if (cleanedAs === "integrated") {
1169
- try {
1170
- await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
1219
+ if (resolved[1] === "work" && resolved[2] === "review"
1220
+ && resolved[3] === "cleanup") {
1221
+ const reviewRef = resolved[4];
1222
+ if (reviewRef === undefined || resolved.length !== 5) {
1223
+ throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1171
1224
  }
1172
- catch (error) {
1173
- throw cleanupCliError(error, `work-item:${qualified}`);
1225
+ const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1226
+ const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1227
+ if (removal === "dirty") {
1228
+ throw usageError(`ReviewRound worktree is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1174
1229
  }
1230
+ emit(`Cleaned ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1231
+ reviewRound: store.getReviewRound(reference.taskId, reference.localId),
1232
+ worktree: { removal }
1233
+ });
1234
+ return;
1175
1235
  }
1176
- let removal;
1177
- try {
1178
- removal = await workspaceCoordinator.cleanupWorkItem(reference.taskId, reference.localId, cleanedAs);
1179
- }
1180
- catch (error) {
1181
- throw cleanupCliError(error, `work-item:${qualified}`);
1182
- }
1183
- if (removal === "dirty") {
1184
- throw usageError(`WorkItem worktree is dirty and was retained: ${qualified}.`, undefined, cleanupBlockedDetails("dirty-worktree", `work-item:${qualified}`, true));
1236
+ if (resolved[1] === "work" && resolved[2] === "review"
1237
+ && resolved[3] === "preserve") {
1238
+ const reviewRef = resolved[4];
1239
+ if (reviewRef === undefined || resolved.length !== 5) {
1240
+ throw usageError("Task work review preserve usage: yui task work review preserve <task>/<review-round>.");
1241
+ }
1242
+ const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1243
+ const round = preserveReviewRoundWorkspace(reference.taskId, reference.localId, store, { runtime, environment: process.env, yuiHome: home });
1244
+ emit(`Preserved ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1245
+ reviewRound: round
1246
+ });
1247
+ return;
1185
1248
  }
1186
- emit(`Cleaned WorkItem worktree ${qualified} (${cleanedAs})\n`, false, {
1187
- workItem: store.getWorkItem(reference.taskId, reference.localId),
1188
- worktree: { removal, disposition: cleanedAs }
1189
- });
1190
- return;
1191
- }
1192
- if (resolved[1] === "work" && resolved[2] === "review"
1193
- && resolved[3] === "cleanup") {
1194
- const reviewRef = resolved[4];
1195
- if (reviewRef === undefined || resolved.length !== 5) {
1196
- throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1197
- }
1198
- const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1199
- const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1200
- if (removal === "dirty") {
1201
- throw usageError(`ReviewRound worktree is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1202
- }
1203
- emit(`Cleaned ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1204
- reviewRound: store.getReviewRound(reference.taskId, reference.localId),
1205
- worktree: { removal }
1206
- });
1207
- return;
1208
- }
1209
- if (resolved[1] === "work" && resolved[2] === "review"
1210
- && resolved[3] === "preserve") {
1211
- const reviewRef = resolved[4];
1212
- if (reviewRef === undefined || resolved.length !== 5) {
1213
- throw usageError("Task work review preserve usage: yui task work review preserve <task>/<review-round>.");
1214
- }
1215
- const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1216
- const round = preserveReviewRoundWorkspace(reference.taskId, reference.localId, store, { runtime, environment: process.env, yuiHome: home });
1217
- emit(`Preserved ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1218
- reviewRound: round
1219
- });
1220
- return;
1221
- }
1222
- let archiveRemoteDeliveryProof;
1223
- let archiveTaskReviewCandidate;
1224
- if (resolved[1] === "archive") {
1225
- const { taskId, disposition, force } = validateTaskArchiveRequest(resolved.slice(2), store, {
1226
- runtime,
1227
- environment: process.env,
1228
- yuiHome: home
1229
- });
1230
- const task = store.getTask(taskId);
1231
- if (task === null)
1232
- throw new Error(`Task disappeared after archive validation: ${taskId}.`);
1233
- if (force || task.status === "archived") {
1234
- // Archive admission and mandatory audit commit before any fallible
1235
- // filesystem/provider work. Repeats report facts, never replay cleanup.
1236
- const admitted = runTaskCommand(resolved.slice(1), store, {
1237
- runtime, environment: process.env, yuiHome: home
1249
+ let archiveRemoteDeliveryProof;
1250
+ let archiveTaskReviewCandidate;
1251
+ if (resolved[1] === "archive") {
1252
+ const { taskId, disposition, force } = validateTaskArchiveRequest(resolved.slice(2), store, {
1253
+ runtime,
1254
+ environment: process.env,
1255
+ yuiHome: home
1238
1256
  });
1239
- if (force && admitted.kind === "output"
1240
- && admitted.data.changed) {
1241
- await workspaceCoordinator.cleanupArchivedTask(taskId, disposition);
1257
+ const task = store.getTask(taskId);
1258
+ if (task === null)
1259
+ throw new Error(`Task disappeared after archive validation: ${taskId}.`);
1260
+ if (force || task.status === "archived") {
1261
+ // Archive admission and mandatory audit commit before any fallible
1262
+ // filesystem/provider work. Repeats report facts, never replay cleanup.
1263
+ const admitted = runTaskCommand(resolved.slice(1), store, {
1264
+ runtime, environment: process.env, yuiHome: home
1265
+ });
1266
+ if (force && admitted.kind === "output"
1267
+ && admitted.data.changed) {
1268
+ await workspaceCoordinator.cleanupArchivedTask(taskId, disposition);
1269
+ }
1270
+ const current = store.getTask(taskId);
1271
+ const archive = taskArchiveDiagnostics(store, current);
1272
+ emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
1273
+ return;
1242
1274
  }
1243
- const current = store.getTask(taskId);
1244
- const archive = taskArchiveDiagnostics(store, current);
1245
- emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
1246
- return;
1247
- }
1248
- {
1249
- if (disposition === "integrated") {
1250
- archiveTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
1251
- archiveRemoteDeliveryProof = createTaskRemoteDeliveryProof(store, task, archiveTaskReviewCandidate ?? null);
1252
- assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery);
1253
- }
1254
- const workItemIds = store.listManagedWorkspaces(task.id)
1255
- .flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
1256
- for (const workItemId of workItemIds) {
1257
- const item = store.getWorkItem(task.id, workItemId);
1258
- if (item?.status !== "accepted" || disposition !== "integrated")
1259
- continue;
1260
- try {
1261
- await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
1275
+ {
1276
+ if (disposition === "integrated") {
1277
+ archiveTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
1278
+ archiveRemoteDeliveryProof = createTaskRemoteDeliveryProof(store, task, archiveTaskReviewCandidate ?? null);
1279
+ assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery);
1262
1280
  }
1263
- catch (error) {
1264
- throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
1281
+ const workItemIds = store.listManagedWorkspaces(task.id)
1282
+ .flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
1283
+ for (const workItemId of workItemIds) {
1284
+ const item = store.getWorkItem(task.id, workItemId);
1285
+ if (item?.status !== "accepted" || disposition !== "integrated")
1286
+ continue;
1287
+ try {
1288
+ await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
1289
+ }
1290
+ catch (error) {
1291
+ throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
1292
+ }
1293
+ }
1294
+ const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
1295
+ if (cleanup.status === "retained-dirty") {
1296
+ throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1297
+ }
1298
+ if (cleanup.status === "failed") {
1299
+ throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1300
+ }
1301
+ }
1302
+ }
1303
+ let taskRetirementProof;
1304
+ if (resolved[1] === "retire") {
1305
+ const taskId = resolved[2];
1306
+ if (taskId !== undefined && !taskId.startsWith("--")) {
1307
+ const task = store.getTask(taskId);
1308
+ if (task?.status === "active" || task?.status === "draft") {
1309
+ try {
1310
+ taskRetirementProof = await new WorkItemChangeSetManager(store)
1311
+ .assertRetirable(taskId);
1312
+ }
1313
+ catch (error) {
1314
+ throw usageError(error instanceof Error ? error.message : String(error));
1315
+ }
1265
1316
  }
1266
1317
  }
1267
- const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
1268
- if (cleanup.status === "retained-dirty") {
1269
- throw usageError(cleanup.error ?? `Task ${task.id} has dirty managed worktrees and remains terminal.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "dirty-worktree", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1318
+ }
1319
+ assertWorkItemExecutionDependenciesForCommand(resolved, store, process.env);
1320
+ if (resolved[1] === "work" && resolved[2] === "dispatch") {
1321
+ const workItemId = resolved[3];
1322
+ const reference = workItemId === undefined
1323
+ ? null
1324
+ : cliWorkItemReference(workItemId, process.env);
1325
+ const item = reference === null
1326
+ ? null
1327
+ : store.getWorkItem(reference.taskId, reference.localId);
1328
+ const task = item === null ? null : store.getTask(item.taskId);
1329
+ if (item !== null && task !== null) {
1330
+ // Authority and pure Lane-shape checks precede every physical or
1331
+ // durable workspace preparation performed for dispatch.
1332
+ assertTaskDeliveryAuthority(store, process.env, task.id);
1333
+ requireWorkItemAssignee(item);
1334
+ workItemDispatchLanePlan(resolved, store, item);
1335
+ }
1336
+ // A rejected Candidate starts a new execution iteration. Release every
1337
+ // terminal Lane Role runtime before preparing the new Lane workspaces;
1338
+ // durable AgentRuns, Groups, Candidates, and workspace owners remain intact.
1339
+ if (item?.status === "open"
1340
+ && currentWorkItemExecutionGroup(item)?.lanes.every(({ disposition }) => disposition !== "open")) {
1341
+ await workspaceCoordinator.cleanupWorkItemRuntime(item.taskId, item.id);
1342
+ }
1343
+ // Every Task needs an authoritative runtime owner before dispatch. A
1344
+ // Gitless Task uses an empty Task-owned view; Project-backed WorkItems
1345
+ // additionally receive their isolated Develop owner below.
1346
+ if (item !== null && task !== null) {
1347
+ await workspacePreparer.prepareTaskWorkspace(task.id);
1270
1348
  }
1271
- if (cleanup.status === "failed") {
1272
- throw usageError(`Task ${task.id} worktree cleanup failed: ${cleanup.error ?? "unknown error"}.`, undefined, cleanupBlockedDetails(cleanup.reason ?? "cleanup-failed", cleanup.resource ?? `task:${task.id}`, cleanup.retryable ?? true, cleanup.checks));
1349
+ // A Project-backed Worker WorkItem gets an isolated Develop owner before
1350
+ // its Lane is prepared. A Leader-owned WorkItem intentionally executes
1351
+ // in the Task main worktree and must not enter this isolation path.
1352
+ if (item !== null
1353
+ && task !== null
1354
+ && task.projectBindings.length > 0
1355
+ && item.assignee !== "leader"
1356
+ && store.getWorkItemWorkspace(task.id, item.id) === null) {
1357
+ await workspaceCoordinator.isolateWorkItem(item.taskId, item.id);
1358
+ }
1359
+ if (item !== null && task !== null) {
1360
+ // For a new Group the preparer has already created deterministic
1361
+ // worktrees, but the owner record is adopted by dispatch's aggregate
1362
+ // transaction once its exact Lane ids exist.
1273
1363
  }
1274
1364
  }
1275
- }
1276
- let taskRetirementProof;
1277
- if (resolved[1] === "retire") {
1278
- const taskId = resolved[2];
1279
- if (taskId !== undefined && !taskId.startsWith("--")) {
1280
- const task = store.getTask(taskId);
1281
- if (task?.status === "active" || task?.status === "draft") {
1365
+ let executionLaneWorkspaces;
1366
+ // Held only for a new Group's dispatch: the per-Project maintenance fence
1367
+ // spans Lane preparation and the adoption transaction, and projectPaths is
1368
+ // the under-fence snapshot the adoption CAS revalidates.
1369
+ let laneDispatchRelease;
1370
+ let laneDispatchProjectPaths;
1371
+ let workItemIntegrationProof;
1372
+ if (resolved[1] === "work" && resolved[2] === "accept") {
1373
+ const workItemId = resolved[3];
1374
+ if (workItemId !== undefined && !workItemId.startsWith("--")) {
1282
1375
  try {
1283
- taskRetirementProof = await new WorkItemChangeSetManager(store)
1284
- .assertRetirable(taskId);
1376
+ const reference = cliWorkItemReference(workItemId, process.env);
1377
+ workItemIntegrationProof = await new WorkItemChangeSetManager(store)
1378
+ .assertIntegrated(reference.taskId, reference.localId, optionValue(resolved, "--candidate")) ?? undefined;
1285
1379
  }
1286
1380
  catch (error) {
1287
1381
  throw usageError(error instanceof Error ? error.message : String(error));
1288
1382
  }
1289
1383
  }
1290
1384
  }
1291
- }
1292
- assertWorkItemExecutionDependenciesForCommand(resolved, store, process.env);
1293
- if (resolved[1] === "work" && resolved[2] === "dispatch") {
1294
- const workItemId = resolved[3];
1295
- const reference = workItemId === undefined
1296
- ? null
1297
- : cliWorkItemReference(workItemId, process.env);
1298
- const item = reference === null
1299
- ? null
1300
- : store.getWorkItem(reference.taskId, reference.localId);
1301
- const task = item === null ? null : store.getTask(item.taskId);
1302
- if (item !== null && task !== null) {
1303
- // Authority and pure Lane-shape checks precede every physical or
1304
- // durable workspace preparation performed for dispatch.
1305
- assertTaskDeliveryAuthority(store, process.env, task.id);
1306
- requireWorkItemAssignee(item);
1307
- workItemDispatchLanePlan(resolved, store, item);
1308
- }
1309
- // A rejected Candidate starts a new execution iteration. Release every
1310
- // terminal Lane Role runtime before preparing the new Lane workspaces;
1311
- // durable AgentRuns, Groups, Candidates, and workspace owners remain intact.
1312
- if (item?.status === "open"
1313
- && currentWorkItemExecutionGroup(item)?.lanes.every(({ disposition }) => disposition !== "open")) {
1314
- await workspaceCoordinator.cleanupWorkItemRuntime(item.taskId, item.id);
1315
- }
1316
- // Every Task needs an authoritative runtime owner before dispatch. A
1317
- // Gitless Task uses an empty Task-owned view; Project-backed WorkItems
1318
- // additionally receive their isolated Develop owner below.
1319
- if (item !== null && task !== null) {
1320
- await workspacePreparer.prepareTaskWorkspace(task.id);
1321
- }
1322
- // A Project-backed Worker WorkItem gets an isolated Develop owner before
1323
- // its Lane is prepared. A Leader-owned WorkItem intentionally executes
1324
- // in the Task main worktree and must not enter this isolation path.
1325
- if (item !== null
1326
- && task !== null
1327
- && task.projectBindings.length > 0
1328
- && item.assignee !== "leader"
1329
- && store.getWorkItemWorkspace(task.id, item.id) === null) {
1330
- await workspaceCoordinator.isolateWorkItem(item.taskId, item.id);
1331
- }
1332
- if (item !== null && task !== null) {
1333
- // For a new Group the preparer has already created deterministic
1334
- // worktrees, but the owner record is adopted by dispatch's aggregate
1335
- // transaction once its exact Lane ids exist.
1385
+ let completionSummary;
1386
+ let completionPublishedTreeProof;
1387
+ if (resolved[1] === "base" && resolved[2] === "status") {
1388
+ const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
1389
+ emit(result.output, false, result.data);
1390
+ return;
1336
1391
  }
1337
- }
1338
- let executionLaneWorkspaces;
1339
- // Held only for a new Group's dispatch: the per-Project maintenance fence
1340
- // spans Lane preparation and the adoption transaction, and projectPaths is
1341
- // the under-fence snapshot the adoption CAS revalidates.
1342
- let laneDispatchRelease;
1343
- let laneDispatchProjectPaths;
1344
- let workItemIntegrationProof;
1345
- if (resolved[1] === "work" && resolved[2] === "accept") {
1346
- const workItemId = resolved[3];
1347
- if (workItemId !== undefined && !workItemId.startsWith("--")) {
1348
- try {
1349
- const reference = cliWorkItemReference(workItemId, process.env);
1350
- workItemIntegrationProof = await new WorkItemChangeSetManager(store)
1351
- .assertIntegrated(reference.taskId, reference.localId, optionValue(resolved, "--candidate")) ?? undefined;
1352
- }
1353
- catch (error) {
1354
- throw usageError(error instanceof Error ? error.message : String(error));
1355
- }
1392
+ if (resolved[1] === "upstream") {
1393
+ const result = await runTaskUpstreamCommand(resolved.slice(2), store, home, {
1394
+ environment: process.env,
1395
+ jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
1396
+ });
1397
+ emit(result.output, false, result.data);
1398
+ return;
1356
1399
  }
1357
- }
1358
- let completionSummary;
1359
- let completionPublishedTreeProof;
1360
- if (resolved[1] === "base" && resolved[2] === "status") {
1361
- const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
1362
- emit(result.output, false, result.data);
1363
- return;
1364
- }
1365
- if (resolved[1] === "upstream") {
1366
- const result = await runTaskUpstreamCommand(resolved.slice(2), store, home, {
1367
- environment: process.env
1368
- });
1369
- emit(result.output, false, result.data);
1370
- return;
1371
- }
1372
- if (resolved[1] === "complete" && resolved[2] !== undefined) {
1373
- const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
1374
- completionSummary = completionRequest.summary;
1375
- const refreshRemote = resolved.includes("--refresh-remote");
1376
- const completion = preflightTaskCompletion(resolved[2], store, {
1377
- environment: process.env,
1378
- ...(taskFinalReviewContract === undefined
1379
- ? {}
1380
- : { taskFinalReviewContract })
1381
- }, completionRequest);
1382
- if (!completion.completed && !completion.activeTaskReview) {
1383
- // An explicit refresh must fetch the remote object graph before the
1384
- // Publication proof resolves its exact commit. Without the flag the
1385
- // command remains offline and preserves the existing proof-first path.
1386
- const refreshedFreshness = refreshRemote
1387
- ? await inspectTaskBaseFreshness(resolved[2], store, { refresh: true })
1388
- : undefined;
1389
- if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
1390
- completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
1391
- }
1392
- const freshness = refreshedFreshness
1393
- ?? await inspectTaskBaseFreshness(resolved[2], store);
1394
- for (const warning of assertTaskBaseFreshnessForCompletion(freshness, {
1395
- ...(completionPublishedTreeProof === undefined
1400
+ if (resolved[1] === "complete" && resolved[2] !== undefined) {
1401
+ const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
1402
+ completionSummary = completionRequest.summary;
1403
+ const refreshRemote = resolved.includes("--refresh-remote");
1404
+ const completion = preflightTaskCompletion(resolved[2], store, {
1405
+ environment: process.env,
1406
+ ...(taskFinalReviewContract === undefined
1396
1407
  ? {}
1397
- : {
1398
- acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
1399
- })
1400
- })) {
1401
- process.stderr.write(`Warning: ${warning}\n`);
1402
- }
1403
- // Keep completion offline by default. An explicit refresh is the only
1404
- // path that may fetch and reconcile a moved remote baseline.
1405
- if (refreshRemote) {
1406
- const reconciled = await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env }) });
1407
- if (reconciled.length > 0) {
1408
- const updates = reconciled.map((entry) => (`${entry.projectId}: ${entry.fromCommit} -> ${entry.toCommit} `
1409
- + `(Integration ${entry.integrationId})`)).join("; ");
1410
- throw usageError(`Remote baseline reconciliation advanced Task ${resolved[2]} (${updates}). `
1411
- + "The Task remains active so the Leader can inspect the new authoritative head, "
1412
- + "decide how prior Review evidence applies, and retry task complete.");
1408
+ : { taskFinalReviewContract })
1409
+ }, completionRequest);
1410
+ if (!completion.completed && !completion.activeTaskReview) {
1411
+ // An explicit refresh must fetch the remote object graph before the
1412
+ // Publication proof resolves its exact commit. Without the flag the
1413
+ // command remains offline and preserves the existing proof-first path.
1414
+ const refreshedFreshness = refreshRemote
1415
+ ? await inspectTaskBaseFreshness(resolved[2], store, { refresh: true })
1416
+ : undefined;
1417
+ if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
1418
+ completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
1419
+ }
1420
+ const freshness = refreshedFreshness
1421
+ ?? await inspectTaskBaseFreshness(resolved[2], store);
1422
+ for (const warning of assertTaskBaseFreshnessForCompletion(freshness, {
1423
+ ...(completionPublishedTreeProof === undefined
1424
+ ? {}
1425
+ : {
1426
+ acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
1427
+ })
1428
+ })) {
1429
+ process.stderr.write(`Warning: ${warning}\n`);
1430
+ }
1431
+ // Keep completion offline by default. An explicit refresh is the only
1432
+ // path that may fetch and reconcile a moved remote baseline.
1433
+ if (refreshRemote) {
1434
+ const reconciled = await reconcileTaskRemoteBaselines(resolved[2], store, home, { environment: process.env, jobPort: createControllerIntegrationJobPort(home, { environment: process.env }) });
1435
+ if (reconciled.length > 0) {
1436
+ const updates = reconciled.map((entry) => (`${entry.projectId}: ${entry.fromCommit} -> ${entry.toCommit} `
1437
+ + `(Integration ${entry.integrationId})`)).join("; ");
1438
+ throw usageError(`Remote baseline reconciliation advanced Task ${resolved[2]} (${updates}). `
1439
+ + "The Task remains active so the Leader can inspect the new authoritative head, "
1440
+ + "decide how prior Review evidence applies, and retry task complete.");
1441
+ }
1413
1442
  }
1414
1443
  }
1415
1444
  }
1416
- }
1417
- let releaseReviewHandoverLock;
1418
- if ((resolved[1] === "review"
1419
- && ["request", "retry"].includes(resolved[2] ?? ""))
1420
- || resolved[1] === "complete") {
1421
- const handoverLock = acquireHandoverLock(home);
1422
- releaseReviewHandoverLock = handoverLock.release;
1423
- }
1424
- try {
1425
- const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
1426
- if (preparedLanes !== undefined) {
1427
- executionLaneWorkspaces = preparedLanes.workspaces;
1428
- laneDispatchRelease = preparedLanes.release;
1429
- laneDispatchProjectPaths = preparedLanes.projectPaths;
1430
- }
1431
- const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1432
- const actualTaskReviewCandidate = archiveRemoteDeliveryProof === undefined
1433
- ? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
1434
- : archiveTaskReviewCandidate;
1435
- const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
1436
- // Read-only Host evidence for Session inspect and Role status/list. The command stays
1437
- // synchronous over persisted state; this is the live reading it prints
1438
- // beside those facts, prepared here because the Host is reached over a
1439
- // socket.
1440
- const liveHostObservations = await liveHostObservationsForTaskCommand(resolved, store, home);
1441
- const liveRunConfiguration = runConfigurationForHostObservation(liveHostObservations?.[resolved[5] ?? ""]);
1442
- // Physical preparation may precede the durable write, but Task status,
1443
- // workspace identity/cwd, and ManagedWorkspace ownership are adopted by
1444
- // one transaction. A failed attempt therefore leaves the Task Draft and
1445
- // owning no writable workspace.
1446
- let taskWorkspaceActivation;
1447
- if (resolved[1] === "activate" && resolved.length === 3) {
1448
- const taskId = resolved[2];
1449
- const task = taskId === undefined ? null : store.getTask(taskId);
1450
- if (task !== null && task.status === "draft") {
1451
- taskLocalActor(store, process.env, task.id);
1452
- taskWorkspaceActivation = await workspacePreparer.activateTaskWorkspace(task.id, process.env);
1453
- }
1454
- }
1455
- const result = runTaskCommand(resolved.slice(1), store, {
1456
- runtime,
1457
- environment: process.env,
1458
- yuiHome: home,
1459
- ...(taskFinalReviewContract === undefined
1460
- ? {}
1461
- : { taskFinalReviewContract }),
1462
- ...(completionSummary === undefined ? {} : { completionSummary }),
1463
- ...(completionPublishedTreeProof === undefined
1464
- ? {}
1465
- : { completionPublishedTreeProof }),
1466
- ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
1467
- ...candidateSnapshots,
1468
- ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1469
- ...(taskWorkspaceActivation === undefined ? {} : { taskWorkspaceActivation }),
1470
- ...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
1471
- ...(actualTaskReviewCandidate === undefined
1472
- ? {}
1473
- : { actualTaskReviewCandidate }),
1474
- ...(archiveRemoteDeliveryProof === undefined
1475
- ? {}
1476
- : { archiveRemoteDeliveryProof }),
1477
- ...(deltaRecheckPreflight === undefined
1478
- ? {}
1479
- : { deltaRecheckPreflight }),
1480
- ...(liveRunConfiguration === undefined
1481
- ? {}
1482
- : { liveRunConfiguration }),
1483
- ...(liveHostObservations === undefined ? {} : { liveHostObservations }),
1484
- ...(taskRetirementProof === undefined ? {} : { taskRetirementProof }),
1485
- ...(validateAgentConfiguration === undefined
1486
- ? {}
1487
- : { validateAgentConfiguration })
1488
- });
1489
- // The dispatch transaction has now adopted (or rejected) the prepared
1490
- // Lane workspaces. Release the held fence so later output/review
1491
- // handling can take the per-Project fence itself.
1492
- if (laneDispatchRelease !== undefined) {
1493
- laneDispatchRelease();
1494
- laneDispatchRelease = undefined;
1445
+ let releaseReviewHandoverLock;
1446
+ if ((resolved[1] === "review"
1447
+ && ["request", "retry"].includes(resolved[2] ?? ""))
1448
+ || resolved[1] === "complete") {
1449
+ const handoverLock = acquireHandoverLock(home);
1450
+ releaseReviewHandoverLock = handoverLock.release;
1495
1451
  }
1496
- if (result.kind === "output") {
1497
- const requestedRound = reviewRoundFromCommandData(result.data);
1498
- const persistedRequestedRound = requestedRound === undefined
1499
- ? null
1500
- : store.getReviewRound(requestedRound.taskId, requestedRound.id);
1501
- let reviewOutput = "";
1502
- let reviewData;
1503
- const resumesReviewDispatch = (resolved[1] === "review" && resolved[2] === "request")
1504
- || (resolved[1] === "work" && resolved[2] === "review")
1505
- || (resolved[1] === "run" && resolved[2] === "retry");
1506
- const reviewDispatchNeeded = requestedRound?.status === "pending"
1507
- || (requestedRound?.status === "running"
1508
- && resumesReviewDispatch
1509
- && persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
1510
- && (lane.currentRunId === undefined
1511
- || store.getRun(requestedRound.taskId, lane.currentRunId)?.status === "failed"))) === true);
1512
- if (reviewDispatchNeeded) {
1513
- try {
1514
- const workspace = requestedRound.status === "running"
1515
- ? store.getReviewRoundWorkspace(requestedRound.taskId, requestedRound.id)
1516
- : await workspacePreparer.prepareReviewRoundWorkspace(requestedRound.taskId, requestedRound.id);
1517
- if (workspace === null) {
1518
- throw new Error(`ReviewRound workspace is not ready: ${requestedRound.id}.`);
1519
- }
1520
- const reviewLaneWorkspaces = await prepareReviewLaneWorkspaces(requestedRound.taskId, requestedRound.id, store, workspacePreparer);
1521
- if (reviewLaneWorkspaces !== undefined) {
1522
- executionLaneWorkspaces = reviewLaneWorkspaces;
1452
+ try {
1453
+ const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
1454
+ if (preparedLanes !== undefined) {
1455
+ executionLaneWorkspaces = preparedLanes.workspaces;
1456
+ laneDispatchRelease = preparedLanes.release;
1457
+ laneDispatchProjectPaths = preparedLanes.projectPaths;
1458
+ }
1459
+ const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1460
+ const actualTaskReviewCandidate = archiveRemoteDeliveryProof === undefined
1461
+ ? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
1462
+ : archiveTaskReviewCandidate;
1463
+ const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
1464
+ // Read-only Host evidence for Session inspect and Role status/list. The command stays
1465
+ // synchronous over persisted state; this is the live reading it prints
1466
+ // beside those facts, prepared here because the Host is reached over a
1467
+ // socket.
1468
+ const liveHostObservations = await liveHostObservationsForTaskCommand(resolved, store, home);
1469
+ const liveRunConfiguration = runConfigurationForHostObservation(liveHostObservations?.[resolved[5] ?? ""]);
1470
+ // Physical preparation may precede the durable write, but Task status,
1471
+ // workspace identity/cwd, and ManagedWorkspace ownership are adopted by
1472
+ // one transaction. A failed attempt therefore leaves the Task Draft and
1473
+ // owning no writable workspace.
1474
+ let taskWorkspaceActivation;
1475
+ if (resolved[1] === "activate" && resolved.length === 3) {
1476
+ const taskId = resolved[2];
1477
+ const task = taskId === undefined ? null : store.getTask(taskId);
1478
+ if (task !== null && task.status === "draft") {
1479
+ taskLocalActor(store, process.env, task.id);
1480
+ taskWorkspaceActivation = await workspacePreparer.activateTaskWorkspace(task.id, process.env);
1481
+ }
1482
+ }
1483
+ const result = runTaskCommand(resolved.slice(1), store, {
1484
+ runtime,
1485
+ environment: process.env,
1486
+ yuiHome: home,
1487
+ ...(taskFinalReviewContract === undefined
1488
+ ? {}
1489
+ : { taskFinalReviewContract }),
1490
+ ...(completionSummary === undefined ? {} : { completionSummary }),
1491
+ ...(completionPublishedTreeProof === undefined
1492
+ ? {}
1493
+ : { completionPublishedTreeProof }),
1494
+ ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
1495
+ ...candidateSnapshots,
1496
+ ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1497
+ ...(taskWorkspaceActivation === undefined ? {} : { taskWorkspaceActivation }),
1498
+ ...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
1499
+ ...(actualTaskReviewCandidate === undefined
1500
+ ? {}
1501
+ : { actualTaskReviewCandidate }),
1502
+ ...(archiveRemoteDeliveryProof === undefined
1503
+ ? {}
1504
+ : { archiveRemoteDeliveryProof }),
1505
+ ...(deltaRecheckPreflight === undefined
1506
+ ? {}
1507
+ : { deltaRecheckPreflight }),
1508
+ ...(liveRunConfiguration === undefined
1509
+ ? {}
1510
+ : { liveRunConfiguration }),
1511
+ ...(liveHostObservations === undefined ? {} : { liveHostObservations }),
1512
+ ...(taskRetirementProof === undefined ? {} : { taskRetirementProof }),
1513
+ ...(validateAgentConfiguration === undefined
1514
+ ? {}
1515
+ : { validateAgentConfiguration })
1516
+ });
1517
+ // The dispatch transaction has now adopted (or rejected) the prepared
1518
+ // Lane workspaces. Release the held fence so later output/review
1519
+ // handling can take the per-Project fence itself.
1520
+ if (laneDispatchRelease !== undefined) {
1521
+ laneDispatchRelease();
1522
+ laneDispatchRelease = undefined;
1523
+ }
1524
+ if (result.kind === "output") {
1525
+ const requestedRound = reviewRoundFromCommandData(result.data);
1526
+ const persistedRequestedRound = requestedRound === undefined
1527
+ ? null
1528
+ : store.getReviewRound(requestedRound.taskId, requestedRound.id);
1529
+ let reviewOutput = "";
1530
+ let reviewData;
1531
+ const resumesReviewDispatch = (resolved[1] === "review" && resolved[2] === "request")
1532
+ || (resolved[1] === "work" && resolved[2] === "review")
1533
+ || (resolved[1] === "run" && resolved[2] === "retry");
1534
+ const reviewDispatchNeeded = requestedRound?.status === "pending"
1535
+ || (requestedRound?.status === "running"
1536
+ && resumesReviewDispatch
1537
+ && persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
1538
+ && (lane.currentRunId === undefined
1539
+ || store.getRun(requestedRound.taskId, lane.currentRunId)?.status === "failed"))) === true);
1540
+ if (reviewDispatchNeeded) {
1541
+ try {
1542
+ const workspace = requestedRound.status === "running"
1543
+ ? store.getReviewRoundWorkspace(requestedRound.taskId, requestedRound.id)
1544
+ : await workspacePreparer.prepareReviewRoundWorkspace(requestedRound.taskId, requestedRound.id);
1545
+ if (workspace === null) {
1546
+ throw new Error(`ReviewRound workspace is not ready: ${requestedRound.id}.`);
1547
+ }
1548
+ const reviewLaneWorkspaces = await prepareReviewLaneWorkspaces(requestedRound.taskId, requestedRound.id, store, workspacePreparer);
1549
+ if (reviewLaneWorkspaces !== undefined) {
1550
+ executionLaneWorkspaces = reviewLaneWorkspaces;
1551
+ }
1552
+ const storedRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
1553
+ const freshTaskCandidate = storedRound?.scope === "task"
1554
+ ? await snapshotActualTaskReviewCandidate(requestedRound.taskId, store, workspacePreparer)
1555
+ : undefined;
1556
+ const run = dispatchPreparedReviewRound(requestedRound.taskId, requestedRound.id, store, {
1557
+ runtime,
1558
+ environment: process.env,
1559
+ yuiHome: home,
1560
+ ...(taskFinalReviewContract === undefined
1561
+ ? {}
1562
+ : { taskFinalReviewContract }),
1563
+ ...(freshTaskCandidate === undefined
1564
+ ? {}
1565
+ : { actualTaskReviewCandidate: freshTaskCandidate }),
1566
+ ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1567
+ ...(storedRound?.deltaRecheck === undefined
1568
+ || deltaRecheckPreflight === undefined
1569
+ ? {}
1570
+ : {
1571
+ deltaRecheckDiff: deltaRecheckPreflight.diffByProject
1572
+ })
1573
+ });
1574
+ reviewOutput = run === null
1575
+ ? `Review ${requestedRound.id} remains running\n`
1576
+ : `Review queued as ${requestedRound.id} (${run.id})\n`;
1577
+ reviewData = {
1578
+ reviewRequest: run === null
1579
+ ? {
1580
+ kind: "running",
1581
+ reviewerRoleName: requestedRound.reviewerRoleName,
1582
+ activeReviewRoundId: requestedRound.id,
1583
+ retryable: false
1584
+ }
1585
+ : {
1586
+ kind: "started",
1587
+ reviewerRoleName: requestedRound.reviewerRoleName,
1588
+ reviewRoundId: requestedRound.id,
1589
+ runId: run.id
1590
+ },
1591
+ reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
1592
+ ...(run === null ? {} : { reviewRun: run }),
1593
+ workspace
1594
+ };
1523
1595
  }
1524
- const storedRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
1525
- const freshTaskCandidate = (storedRound?.scope ?? "work-item") === "task"
1526
- ? await snapshotActualTaskReviewCandidate(requestedRound.taskId, store, workspacePreparer)
1527
- : undefined;
1528
- const run = dispatchPreparedReviewRound(requestedRound.taskId, requestedRound.id, store, {
1529
- runtime,
1530
- environment: process.env,
1531
- yuiHome: home,
1532
- ...(taskFinalReviewContract === undefined
1533
- ? {}
1534
- : { taskFinalReviewContract }),
1535
- ...(freshTaskCandidate === undefined
1536
- ? {}
1537
- : { actualTaskReviewCandidate: freshTaskCandidate }),
1538
- ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1539
- ...(storedRound?.deltaRecheck === undefined
1540
- || deltaRecheckPreflight === undefined
1541
- ? {}
1542
- : {
1543
- deltaRecheckDiff: deltaRecheckPreflight.diffByProject
1544
- })
1545
- });
1546
- reviewOutput = run === null
1547
- ? `Review ${requestedRound.id} remains running\n`
1548
- : `Review queued as ${requestedRound.id} (${run.id})\n`;
1549
- reviewData = {
1550
- reviewRequest: run === null
1551
- ? {
1552
- kind: "running",
1596
+ catch (error) {
1597
+ const message = error instanceof Error ? error.message : String(error);
1598
+ await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1599
+ const failed = failPendingReviewRound(requestedRound.taskId, requestedRound.id, message, store, {
1600
+ runtime,
1601
+ environment: process.env,
1602
+ yuiHome: home
1603
+ });
1604
+ reviewOutput = `Review could not start: ${message}\n`
1605
+ + "The failed ReviewRound was retained for Leader routing.\n";
1606
+ reviewData = {
1607
+ reviewRequest: {
1608
+ kind: "unavailable",
1553
1609
  reviewerRoleName: requestedRound.reviewerRoleName,
1554
- activeReviewRoundId: requestedRound.id,
1555
- retryable: false
1556
- }
1557
- : {
1558
- kind: "started",
1559
- reviewerRoleName: requestedRound.reviewerRoleName,
1560
- reviewRoundId: requestedRound.id,
1561
- runId: run.id
1610
+ reviewRoundId: failed.id,
1611
+ reason: message,
1612
+ retryable: true
1562
1613
  },
1563
- reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
1564
- ...(run === null ? {} : { reviewRun: run }),
1565
- workspace
1566
- };
1614
+ reviewRound: failed
1615
+ };
1616
+ }
1567
1617
  }
1568
- catch (error) {
1569
- const message = error instanceof Error ? error.message : String(error);
1570
- await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1571
- const failed = failPendingReviewRound(requestedRound.taskId, requestedRound.id, message, store, {
1572
- runtime,
1573
- environment: process.env,
1574
- yuiHome: home
1575
- });
1576
- reviewOutput = `Review could not start: ${message}\n`
1577
- + "The failed ReviewRound was retained for Leader routing.\n";
1578
- reviewData = {
1579
- reviewRequest: {
1580
- kind: "unavailable",
1581
- reviewerRoleName: requestedRound.reviewerRoleName,
1582
- reviewRoundId: failed.id,
1583
- reason: message,
1584
- retryable: true
1585
- },
1586
- reviewRound: failed
1587
- };
1618
+ if (resolved[1] === "project" && resolved[2] === "add") {
1619
+ const taskId = resolved[3];
1620
+ const task = taskId === undefined ? null : store.getTask(taskId);
1621
+ if (task?.status === "active") {
1622
+ await workspacePreparer.prepareTaskWorkspace(task.id);
1623
+ }
1588
1624
  }
1589
- }
1590
- if (resolved[1] === "project" && resolved[2] === "add") {
1591
- const taskId = resolved[3];
1592
- const task = taskId === undefined ? null : store.getTask(taskId);
1593
- if (task?.status === "active") {
1594
- await workspacePreparer.prepareTaskWorkspace(task.id);
1625
+ const controlData = result.data;
1626
+ const failureCode = controlData?.steer?.code ?? controlData?.interrupt?.code;
1627
+ if (failureCode !== undefined) {
1628
+ emitControlFailure(result.output, failureCode, result.data);
1629
+ return;
1595
1630
  }
1596
- }
1597
- const controlData = result.data;
1598
- const failureCode = controlData?.steer?.code ?? controlData?.interrupt?.code;
1599
- if (failureCode !== undefined) {
1600
- emitControlFailure(result.output, failureCode, result.data);
1631
+ emit(`${result.output}${reviewOutput}`, false, reviewData === undefined
1632
+ ? result.data
1633
+ : { command: result.data, ...reviewData });
1601
1634
  return;
1602
1635
  }
1603
- emit(`${result.output}${reviewOutput}`, false, reviewData === undefined
1604
- ? result.data
1605
- : { command: result.data, ...reviewData });
1606
- return;
1607
- }
1608
- if (jsonOutput && result.kind !== "session-stop"
1609
- && result.kind !== "input-steer" && result.kind !== "input-interrupt") {
1610
- throw usageError("Task Role view/takeover requires an interactive terminal.");
1611
- }
1612
- if (result.kind === "session-stop") {
1613
- await ensureFileTaskController(home, { environment: process.env });
1614
- try {
1615
- await runtime.stopExactTaskRoleSession({
1636
+ if (jsonOutput && result.kind !== "session-stop"
1637
+ && result.kind !== "input-steer" && result.kind !== "input-interrupt") {
1638
+ throw usageError("Task Role view/takeover requires an interactive terminal.");
1639
+ }
1640
+ if (result.kind === "session-stop") {
1641
+ await ensureFileTaskController(home, { environment: process.env });
1642
+ try {
1643
+ await runtime.stopExactTaskRoleSession({
1644
+ taskId: result.taskId,
1645
+ roleName: result.roleName,
1646
+ agentId: result.agentId,
1647
+ adapterId: result.adapterId,
1648
+ nativeSessionId: result.nativeSessionId,
1649
+ sessionUpdatedAt: result.sessionUpdatedAt
1650
+ });
1651
+ }
1652
+ catch (error) {
1653
+ throw runtimeError(`Session stop was requested but physical Host cleanup did not complete: ${error instanceof Error ? error.message : String(error)}`);
1654
+ }
1655
+ emit(result.output, false, {
1616
1656
  taskId: result.taskId,
1617
1657
  roleName: result.roleName,
1618
- agentId: result.agentId,
1619
- adapterId: result.adapterId,
1620
- nativeSessionId: result.nativeSessionId,
1621
- sessionUpdatedAt: result.sessionUpdatedAt
1658
+ stopped: true,
1659
+ reason: result.reason
1622
1660
  });
1661
+ return;
1623
1662
  }
1624
- catch (error) {
1625
- throw runtimeError(`Session stop was requested but physical Host cleanup did not complete: ${error instanceof Error ? error.message : String(error)}`);
1663
+ if (result.kind === "view") {
1664
+ if (result.output !== undefined)
1665
+ emit(result.output);
1666
+ tmux.attachRole(result.taskId, result.roleName, "read-only");
1667
+ return;
1626
1668
  }
1627
- emit(result.output, false, {
1628
- taskId: result.taskId,
1629
- roleName: result.roleName,
1630
- stopped: true,
1631
- reason: result.reason
1632
- });
1633
- return;
1634
- }
1635
- if (result.kind === "view") {
1636
- if (result.output !== undefined)
1637
- emit(result.output);
1638
- tmux.attachRole(result.taskId, result.roleName, "read-only");
1639
- return;
1640
- }
1641
- if (result.kind === "input-steer") {
1642
- // Core already persisted the Message and proved target + capability +
1643
- // writer fence. This is the single live edge: one native steer of the
1644
- // exact current Turn, with no retarget and no fallback to interrupt or
1645
- // queue. Its durable settlement flows through the steer receipt fold.
1646
- await ensureFileTaskController(home, { environment: process.env });
1647
- let control;
1648
- try {
1649
- control = await sendAgentHostSteerControl({
1650
- home,
1651
- scope: "task",
1652
- taskId: result.taskId,
1653
- roleName: result.roleName,
1654
- control: {
1655
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1656
- type: "steer-turn",
1657
- nativeSessionId: result.target.nativeSessionId,
1658
- nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
1659
- authority: result.target.authority,
1660
- run: { attemptId: result.receiptId, boundedText: result.text }
1661
- }
1669
+ if (result.kind === "input-steer") {
1670
+ // Core already persisted the Message and proved target + capability +
1671
+ // writer fence. This is the single live edge: one native steer of the
1672
+ // exact current Turn, with no retarget and no fallback to interrupt or
1673
+ // queue. Its durable settlement flows through the steer receipt fold.
1674
+ await ensureFileTaskController(home, { environment: process.env });
1675
+ let control;
1676
+ try {
1677
+ control = await sendAgentHostSteerControl({
1678
+ home,
1679
+ scope: "task",
1680
+ taskId: result.taskId,
1681
+ roleName: result.roleName,
1682
+ control: {
1683
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1684
+ type: "steer-turn",
1685
+ nativeSessionId: result.target.nativeSessionId,
1686
+ nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
1687
+ authority: result.target.authority,
1688
+ run: { attemptId: result.receiptId, boundedText: result.text }
1689
+ }
1690
+ });
1691
+ }
1692
+ catch (error) {
1693
+ throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
1694
+ + `${error instanceof Error ? error.message : String(error)}. `
1695
+ + "The Message is retained and its outcome is recorded from the Host; whether the "
1696
+ + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
1697
+ + "do not reissue the same input under a new requestId or a different action.");
1698
+ }
1699
+ const steer = foldSteerLiveReceipt(control);
1700
+ if (steer.state !== "steered") {
1701
+ emitControlFailure(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_REJECTED", { taskId: result.taskId, roleName: result.roleName, messageId: result.messageId, steer });
1702
+ return;
1703
+ }
1704
+ emit(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), false, {
1705
+ taskId: result.taskId, roleName: result.roleName,
1706
+ messageId: result.messageId, steer
1662
1707
  });
1708
+ return;
1663
1709
  }
1664
- catch (error) {
1665
- throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
1666
- + `${error instanceof Error ? error.message : String(error)}. `
1667
- + "The Message is retained and its outcome is recorded from the Host; whether the "
1668
- + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
1669
- + "do not reissue the same input under a new requestId or a different action.");
1670
- }
1671
- const steer = foldSteerLiveReceipt(control);
1672
- if (steer.state !== "steered") {
1673
- emitControlFailure(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), steer.state === "steer-unknown" ? "DELIVERY_UNKNOWN" : "STEER_REJECTED", { taskId: result.taskId, roleName: result.roleName, messageId: result.messageId, steer });
1710
+ if (result.kind === "input-interrupt") {
1711
+ // The single live edge for interrupt: one native cancel of the exact
1712
+ // current Turn. Never a kill/restart/detach. Any then-handoff was
1713
+ // already claimed durably by Core and is delivered once by the ordinary
1714
+ // continuation path after this Turn reaches a proven terminal.
1715
+ await ensureFileTaskController(home, { environment: process.env });
1716
+ let control;
1717
+ try {
1718
+ control = await sendAgentHostCancelControl({
1719
+ home,
1720
+ scope: "task",
1721
+ taskId: result.taskId,
1722
+ roleName: result.roleName,
1723
+ control: {
1724
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1725
+ type: "cancel",
1726
+ nativeOnly: true,
1727
+ nativeSessionId: result.target.nativeSessionId,
1728
+ // Native cancel names the exact original execution attempt it stops
1729
+ // (Host matches request.attemptId === activeRunAttemptId). That is
1730
+ // distinct from receiptId, the durable identity of this interrupt
1731
+ // control operation — never send the operation id as the turn id.
1732
+ attemptId: result.target.attemptId,
1733
+ authority: result.target.authority
1734
+ }
1735
+ });
1736
+ }
1737
+ catch (error) {
1738
+ recordTaskInterruptResult(store, result.taskId, result.receiptId, { state: "interrupt-unknown", outcome: "cancel-requested" });
1739
+ throw runtimeError(`Interrupt ${result.receiptId} of ${result.taskId}/${result.roleName} did not complete: `
1740
+ + `${error instanceof Error ? error.message : String(error)}. `
1741
+ + "No process was killed; re-read the Session before retrying.");
1742
+ }
1743
+ const interrupt = foldInterruptLiveReceipt(control);
1744
+ recordTaskInterruptResult(store, result.taskId, result.receiptId, interrupt);
1745
+ if (interrupt.state !== "interrupt-requested") {
1746
+ emitControlFailure(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN"
1747
+ : interrupt.state === "interrupt-not-active" ? "NO_ACTIVE_TURN" : "INTERRUPT_REJECTED", { taskId: result.taskId, roleName: result.roleName, receiptId: result.receiptId, interrupt });
1748
+ return;
1749
+ }
1750
+ emit(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), false, {
1751
+ taskId: result.taskId, roleName: result.roleName,
1752
+ ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
1753
+ interrupt
1754
+ });
1674
1755
  return;
1675
1756
  }
1676
- emit(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), false, {
1677
- taskId: result.taskId, roleName: result.roleName,
1678
- messageId: result.messageId, steer
1679
- });
1680
- return;
1681
- }
1682
- if (result.kind === "input-interrupt") {
1683
- // The single live edge for interrupt: one native cancel of the exact
1684
- // current Turn. Never a kill/restart/detach. Any then-handoff was
1685
- // already claimed durably by Core and is delivered once by the ordinary
1686
- // continuation path after this Turn reaches a proven terminal.
1687
- await ensureFileTaskController(home, { environment: process.env });
1688
- let control;
1689
- try {
1690
- control = await sendAgentHostCancelControl({
1691
- home,
1692
- scope: "task",
1757
+ const syncAuthority = async (authorityResult) => {
1758
+ let control;
1759
+ try {
1760
+ control = await sendAgentHostAuthorityControl({
1761
+ home,
1762
+ scope: "task",
1763
+ taskId: authorityResult.taskId,
1764
+ roleName: authorityResult.roleName,
1765
+ control: {
1766
+ protocol: AGENT_HOST_CONTROL_PROTOCOL,
1767
+ type: "set-authority",
1768
+ nativeSessionId: authorityResult.nativeSessionId,
1769
+ authority: authorityResult.authority
1770
+ }
1771
+ });
1772
+ }
1773
+ catch (error) {
1774
+ throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
1775
+ + `${error instanceof Error ? error.message : String(error)}. `
1776
+ + `Durable authority is ${authorityResult.authority.owner}-owned; retry `
1777
+ + `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
1778
+ + "to reconcile the Host.");
1779
+ }
1780
+ if (control.outcome !== "accepted"
1781
+ || control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
1782
+ || control.snapshot.authorityEpoch !== authorityResult.authority.epoch
1783
+ || control.snapshot.authorityOwner !== authorityResult.authority.owner
1784
+ || control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
1785
+ throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
1786
+ + (control.snapshot.detail ?? control.outcome)
1787
+ + `. Durable authority is ${authorityResult.authority.owner}-owned; `
1788
+ + "retry 'yui task role release "
1789
+ + `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
1790
+ }
1791
+ return control;
1792
+ };
1793
+ await syncAuthority(result);
1794
+ emit(result.output);
1795
+ if (result.action === "release") {
1796
+ runtime.notifyMailboxChanged({
1797
+ kind: "role",
1693
1798
  taskId: result.taskId,
1694
- roleName: result.roleName,
1695
- control: {
1696
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1697
- type: "cancel",
1698
- nativeOnly: true,
1699
- nativeSessionId: result.target.nativeSessionId,
1700
- // Native cancel names the exact original execution attempt it stops
1701
- // (Host matches request.attemptId === activeRunAttemptId). That is
1702
- // distinct from receiptId, the durable identity of this interrupt
1703
- // control operation — never send the operation id as the turn id.
1704
- attemptId: result.target.attemptId,
1705
- authority: result.target.authority
1706
- }
1799
+ roleName: result.roleName
1707
1800
  });
1708
- }
1709
- catch (error) {
1710
- recordTaskInterruptResult(store, result.taskId, result.receiptId, { state: "interrupt-unknown", outcome: "cancel-requested" });
1711
- throw runtimeError(`Interrupt ${result.receiptId} of ${result.taskId}/${result.roleName} did not complete: `
1712
- + `${error instanceof Error ? error.message : String(error)}. `
1713
- + "No process was killed; re-read the Session before retrying.");
1714
- }
1715
- const interrupt = foldInterruptLiveReceipt(control);
1716
- recordTaskInterruptResult(store, result.taskId, result.receiptId, interrupt);
1717
- if (interrupt.state !== "interrupt-requested") {
1718
- emitControlFailure(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN"
1719
- : interrupt.state === "interrupt-not-active" ? "NO_ACTIVE_TURN" : "INTERRUPT_REJECTED", { taskId: result.taskId, roleName: result.roleName, receiptId: result.receiptId, interrupt });
1720
1801
  return;
1721
1802
  }
1722
- emit(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), false, {
1723
- taskId: result.taskId, roleName: result.roleName,
1724
- ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
1725
- interrupt
1726
- });
1727
- return;
1728
- }
1729
- const syncAuthority = async (authorityResult) => {
1730
- let control;
1803
+ process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
1804
+ + "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
1731
1805
  try {
1732
- control = await sendAgentHostAuthorityControl({
1733
- home,
1734
- scope: "task",
1735
- taskId: authorityResult.taskId,
1736
- roleName: authorityResult.roleName,
1737
- control: {
1738
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1739
- type: "set-authority",
1740
- nativeSessionId: authorityResult.nativeSessionId,
1741
- authority: authorityResult.authority
1806
+ tmux.attachRole(result.taskId, result.roleName, "read-write");
1807
+ }
1808
+ finally {
1809
+ const currentTask = store.getTask(result.taskId);
1810
+ // Completing or retiring the Task from inside the takeover AgentRun owns
1811
+ // Provider shutdown and clears the live binding. Do not turn that
1812
+ // successful terminal transition into a failing best-effort release.
1813
+ if (currentTask?.status === "active") {
1814
+ const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
1815
+ if (released.kind !== "authority" || released.action !== "release") {
1816
+ throw runtimeError("Provider authority release returned an invalid result.");
1742
1817
  }
1743
- });
1818
+ await syncAuthority(released);
1819
+ emit(released.output);
1820
+ runtime.notifyMailboxChanged({
1821
+ kind: "role",
1822
+ taskId: released.taskId,
1823
+ roleName: released.roleName
1824
+ });
1825
+ }
1744
1826
  }
1745
- catch (error) {
1746
- throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
1747
- + `${error instanceof Error ? error.message : String(error)}. `
1748
- + `Durable authority is ${authorityResult.authority.owner}-owned; retry `
1749
- + `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
1750
- + "to reconcile the Host.");
1751
- }
1752
- if (control.outcome !== "accepted"
1753
- || control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
1754
- || control.snapshot.authorityEpoch !== authorityResult.authority.epoch
1755
- || control.snapshot.authorityOwner !== authorityResult.authority.owner
1756
- || control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
1757
- throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
1758
- + (control.snapshot.detail ?? control.outcome)
1759
- + `. Durable authority is ${authorityResult.authority.owner}-owned; `
1760
- + "retry 'yui task role release "
1761
- + `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
1762
- }
1763
- return control;
1764
- };
1765
- await syncAuthority(result);
1766
- emit(result.output);
1767
- if (result.action === "release") {
1768
- runtime.notifyMailboxChanged({
1769
- kind: "role",
1770
- taskId: result.taskId,
1771
- roleName: result.roleName
1772
- });
1773
1827
  return;
1774
1828
  }
1775
- process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
1776
- + "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
1777
- try {
1778
- tmux.attachRole(result.taskId, result.roleName, "read-write");
1829
+ catch (error) {
1830
+ await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1831
+ if (laneDispatchRelease !== undefined) {
1832
+ laneDispatchRelease();
1833
+ laneDispatchRelease = undefined;
1834
+ }
1835
+ throw error;
1779
1836
  }
1780
1837
  finally {
1781
- const currentTask = store.getTask(result.taskId);
1782
- // Completing or retiring the Task from inside the takeover AgentRun owns
1783
- // Provider shutdown and clears the live binding. Do not turn that
1784
- // successful terminal transition into a failing best-effort release.
1785
- if (currentTask?.status === "active") {
1786
- const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
1787
- if (released.kind !== "authority" || released.action !== "release") {
1788
- throw runtimeError("Provider authority release returned an invalid result.");
1789
- }
1790
- await syncAuthority(released);
1791
- emit(released.output);
1792
- runtime.notifyMailboxChanged({
1793
- kind: "role",
1794
- taskId: released.taskId,
1795
- roleName: released.roleName
1796
- });
1838
+ if (releaseReviewHandoverLock !== undefined) {
1839
+ releaseReviewHandoverLock();
1840
+ releaseReviewHandoverLock = undefined;
1797
1841
  }
1798
1842
  }
1843
+ }
1844
+ if (resolved[0] === "jobs") {
1845
+ emit(runJobCommand(resolved.slice(1), store, { runtime }));
1799
1846
  return;
1800
1847
  }
1801
- catch (error) {
1802
- await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1803
- if (laneDispatchRelease !== undefined) {
1804
- laneDispatchRelease();
1805
- laneDispatchRelease = undefined;
1806
- }
1807
- throw error;
1848
+ if (resolved[0] === "job") {
1849
+ emit(await runDurableJobCommand(resolved.slice(1), {
1850
+ home,
1851
+ json: jsonOutput,
1852
+ environment: process.env,
1853
+ store
1854
+ }));
1855
+ return;
1808
1856
  }
1809
- finally {
1810
- if (releaseReviewHandoverLock !== undefined) {
1811
- releaseReviewHandoverLock();
1812
- releaseReviewHandoverLock = undefined;
1813
- }
1857
+ if (resolved[0] === "telemetry") {
1858
+ emit(await runTelemetryCommand(resolved.slice(1), {
1859
+ home,
1860
+ json: jsonOutput,
1861
+ environment: process.env,
1862
+ store
1863
+ }));
1864
+ return;
1814
1865
  }
1866
+ throw usageError(`Command is not connected to the current TaskStore command routing: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
1815
1867
  }
1816
- if (resolved[0] === "jobs") {
1817
- emit(runJobCommand(resolved.slice(1), store, { runtime }));
1818
- return;
1819
- }
1820
- if (resolved[0] === "job") {
1821
- emit(await runDurableJobCommand(resolved.slice(1), {
1822
- home,
1823
- json: jsonOutput,
1824
- environment: process.env,
1825
- store
1826
- }));
1827
- return;
1828
- }
1829
- if (resolved[0] === "telemetry") {
1830
- emit(await runTelemetryCommand(resolved.slice(1), {
1831
- home,
1832
- json: jsonOutput,
1833
- environment: process.env,
1834
- store
1835
- }));
1836
- return;
1868
+ finally {
1869
+ await telemetry?.sink.close();
1837
1870
  }
1838
- throw usageError(`Command is not connected to the current TaskStore command routing: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
1839
1871
  }
1840
1872
  function explicitReleaseActivationDriver() {
1841
1873
  if (args.length !== 3
@@ -1862,7 +1894,7 @@ async function preflightManagedTaskControlPlane() {
1862
1894
  const internalCallback = args[0] === "internal"
1863
1895
  && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
1864
1896
  const home = resolveYuiHome(process.env);
1865
- const manifest = assertManagedSessionManifest(home, "task");
1897
+ assertManagedSessionManifest(home, "task");
1866
1898
  const diagnosticTarget = taskDiagnosticTarget(args);
1867
1899
  const diagnostic = diagnosticTarget !== undefined
1868
1900
  && diagnosticTarget === process.env.YUI_TASK_ID;
@@ -1937,7 +1969,19 @@ async function preflightManagedGlobalControlPlane() {
1937
1969
  await assertRuntimeCoherence({ actualHome: home }, {
1938
1970
  checkController: !(expectedRoleKind === "operator" && operatorOfflineCommand(args))
1939
1971
  });
1940
- return { contract: undefined, verifiedStore: openCurrentTaskStore(home) };
1972
+ const verifiedStore = openCurrentTaskStore(home);
1973
+ const internalCallback = args[0] === "internal"
1974
+ && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
1975
+ if (!internalCallback && !operatorOfflineCommand(args)) {
1976
+ try {
1977
+ requireManagedGlobalCaller(verifiedStore, process.env);
1978
+ }
1979
+ catch (error) {
1980
+ verifiedStore.close();
1981
+ throw error;
1982
+ }
1983
+ }
1984
+ return { contract: undefined, verifiedStore };
1941
1985
  }
1942
1986
  /**
1943
1987
  * The entry point the current invocation resolves to. Only its executable and
@@ -2159,7 +2203,7 @@ async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, en
2159
2203
  && args[3] !== undefined) {
2160
2204
  const reference = cliTaskRecordReference(args[3], "reviewRound", environment);
2161
2205
  const round = store.getReviewRound(reference.taskId, reference.localId);
2162
- if (round !== null && (round.scope ?? "work-item") === "task") {
2206
+ if (round !== null && round.scope === "task") {
2163
2207
  taskId = reference.taskId;
2164
2208
  }
2165
2209
  }
@@ -2169,7 +2213,7 @@ async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, en
2169
2213
  && args[4] !== undefined) {
2170
2214
  const reference = cliTaskRecordReference(args[4], "reviewRound", environment);
2171
2215
  const round = store.getReviewRound(reference.taskId, reference.localId);
2172
- if (round !== null && (round.scope ?? "work-item") === "task") {
2216
+ if (round !== null && round.scope === "task") {
2173
2217
  taskId = reference.taskId;
2174
2218
  }
2175
2219
  }
@@ -2183,7 +2227,7 @@ async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, en
2183
2227
  : store.getReviewRound(reference.taskId, run.reviewRoundId);
2184
2228
  if (run?.purpose === "review"
2185
2229
  && round !== null
2186
- && (round.scope ?? "work-item") === "task") {
2230
+ && round.scope === "task") {
2187
2231
  taskId = reference.taskId;
2188
2232
  }
2189
2233
  }
@@ -2440,7 +2484,7 @@ async function completionCommand(home, node) {
2440
2484
  const store = openCurrentTaskStore(home);
2441
2485
  const ioHandle = terminalIo();
2442
2486
  try {
2443
- const manager = new FileCompletionManager(store, process.env, resolveCliIdentity(process.env));
2487
+ const manager = new FileCompletionManager(store, process.env);
2444
2488
  emit(await runCompletionWizard(manager, ioHandle.io, shell === undefined ? {} : { shell }));
2445
2489
  }
2446
2490
  finally {
@@ -2521,7 +2565,7 @@ async function preflightAgentConfigurationMutation(commandArgs, store, catalogs)
2521
2565
  const mutation = profileAgentConfigurationMutation(commandArgs, store)
2522
2566
  ?? taskRoleAgentConfigurationMutation(commandArgs, store);
2523
2567
  if (mutation === undefined) {
2524
- await warmLegacyRoleConfigurationMutation(commandArgs, store, catalogs);
2568
+ await warmConfigurationMutationCatalog(commandArgs, store, catalogs);
2525
2569
  return undefined;
2526
2570
  }
2527
2571
  const agent = store.getConfiguredAgent(mutation.agentId);
@@ -2562,7 +2606,7 @@ function taskRoleAgentConfigurationMutation(args, store) {
2562
2606
  ? previewTaskRoleAgentConfigurationMutation(args.slice(1), store)
2563
2607
  : undefined;
2564
2608
  }
2565
- async function warmLegacyRoleConfigurationMutation(commandArgs, store, catalogs) {
2609
+ async function warmConfigurationMutationCatalog(commandArgs, store, catalogs) {
2566
2610
  if (!hasModelOrEffortMutation(commandArgs))
2567
2611
  return;
2568
2612
  const agentId = configurationMutationAgentId(commandArgs, store);
@@ -2849,15 +2893,8 @@ function agentEnvironmentRefreshScope(previous, current, configured) {
2849
2893
  nativeNames: [...new Set([...currentNative, ...previousOnlyNative])]
2850
2894
  };
2851
2895
  }
2852
- export function cliIdentity(env) {
2853
- return env.YUI_CLI_NAME === "yui-dev" ? "yui-dev" : "yui";
2854
- }
2855
2896
  function normalizeAliases(input) {
2856
2897
  const normalized = [...input];
2857
- // Existing immutable Session Manifests (through 0.15.8) name this entry.
2858
- // Remove once those Sessions are retired; both names share one handler.
2859
- if (normalized[0] === "task" && normalized[1] === "turn")
2860
- normalized[1] = "run";
2861
2898
  if (normalized.length === 1 && (normalized[0] === "-v" || normalized[0] === "--version")) {
2862
2899
  return ["version"];
2863
2900
  }