@zq-silk/yui 2.0.0 → 2.2.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 (138) hide show
  1. package/README.md +4 -0
  2. package/dist/cli/commandCatalog.js +318 -289
  3. package/dist/cli/commandDiscovery.js +72 -0
  4. package/dist/cli/completion.js +22 -227
  5. package/dist/cli/dynamicCompletion.js +12 -6
  6. package/dist/cli/invocationAuthority.js +2 -2
  7. package/dist/cli/invocationRouter.js +18 -7
  8. package/dist/cli.js +23 -3030
  9. package/dist/commands/capabilityCommands.js +5 -5
  10. package/dist/commands/globalRoleCommands.js +2 -1
  11. package/dist/commands/taskCommands.js +90 -21
  12. package/dist/commands/taskIntegrationCommands.js +3 -1
  13. package/dist/commands/taskUpstreamCommands.js +3 -1
  14. package/dist/context/runContextPack.js +132 -5
  15. package/dist/context/sourceRunContext.js +4 -2
  16. package/dist/context/taskContext.js +12 -3
  17. package/dist/controlPlaneCli.js +3092 -0
  18. package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
  19. package/dist/controller/jobControl.js +21 -0
  20. package/dist/executor/agentExecutor.js +1 -1
  21. package/dist/executor/effectiveLaunch.js +18 -5
  22. package/dist/executor/fileRoleLaunchPlanner.js +15 -19
  23. package/dist/integration/gitIntegrationService.js +50 -15
  24. package/dist/kernel/accessAssessment.js +1 -0
  25. package/dist/kernel/builtinCapabilities.js +28 -1
  26. package/dist/kernel/capabilityRegistry.js +64 -19
  27. package/dist/message/messageContinuation.js +7 -2
  28. package/dist/nativeAgent/agent.js +275 -0
  29. package/dist/nativeAgent/cliDemo.js +37 -0
  30. package/dist/nativeAgent/codingTools.js +11 -0
  31. package/dist/nativeAgent/commandTool.js +215 -0
  32. package/dist/nativeAgent/compactionDemo.js +158 -0
  33. package/dist/nativeAgent/composition.js +71 -0
  34. package/dist/nativeAgent/context/budget.js +44 -0
  35. package/dist/nativeAgent/context/index.js +339 -0
  36. package/dist/nativeAgent/context/providerCompressor.js +103 -0
  37. package/dist/nativeAgent/contracts.js +1 -0
  38. package/dist/nativeAgent/demo.js +40 -0
  39. package/dist/nativeAgent/evaluation/cases.js +38 -0
  40. package/dist/nativeAgent/evaluation/checks.js +91 -0
  41. package/dist/nativeAgent/evaluation/demo.js +19 -0
  42. package/dist/nativeAgent/evaluation/files.js +54 -0
  43. package/dist/nativeAgent/evaluation/fixture.js +36 -0
  44. package/dist/nativeAgent/evaluation/index.js +239 -0
  45. package/dist/nativeAgent/executionOwner.js +209 -0
  46. package/dist/nativeAgent/filePatterns.js +170 -0
  47. package/dist/nativeAgent/fileToolsSupport.js +202 -0
  48. package/dist/nativeAgent/index.js +16 -0
  49. package/dist/nativeAgent/interaction/cli.js +358 -0
  50. package/dist/nativeAgent/interaction/contracts.js +1 -0
  51. package/dist/nativeAgent/interaction/index.js +3 -0
  52. package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
  53. package/dist/nativeAgent/interaction/renderer.js +34 -0
  54. package/dist/nativeAgent/localSafety.js +258 -0
  55. package/dist/nativeAgent/mockProvider.js +43 -0
  56. package/dist/nativeAgent/model/anthropicMessages.js +204 -0
  57. package/dist/nativeAgent/model/chatCompletions.js +210 -0
  58. package/dist/nativeAgent/model/errors.js +47 -0
  59. package/dist/nativeAgent/model/gateway.js +423 -0
  60. package/dist/nativeAgent/model/index.js +7 -0
  61. package/dist/nativeAgent/model/observationAdapter.js +21 -0
  62. package/dist/nativeAgent/model/protocols.js +19 -0
  63. package/dist/nativeAgent/model/responses.js +263 -0
  64. package/dist/nativeAgent/model/types.js +1 -0
  65. package/dist/nativeAgent/model/wire.js +73 -0
  66. package/dist/nativeAgent/observability/index.js +220 -0
  67. package/dist/nativeAgent/product/catalog.js +82 -0
  68. package/dist/nativeAgent/product/config.js +295 -0
  69. package/dist/nativeAgent/product/facts.js +30 -0
  70. package/dist/nativeAgent/product/index.js +62 -0
  71. package/dist/nativeAgent/product/location.js +44 -0
  72. package/dist/nativeAgent/product/runtime.js +276 -0
  73. package/dist/nativeAgent/product/storage.js +49 -0
  74. package/dist/nativeAgent/product/tools.js +47 -0
  75. package/dist/nativeAgent/product/transport.js +54 -0
  76. package/dist/nativeAgent/projectGuidance/index.js +425 -0
  77. package/dist/nativeAgent/searchTools.js +305 -0
  78. package/dist/nativeAgent/session/backends.js +293 -0
  79. package/dist/nativeAgent/session/catalog.js +97 -0
  80. package/dist/nativeAgent/session/catalogDemo.js +87 -0
  81. package/dist/nativeAgent/session/contracts.js +1 -0
  82. package/dist/nativeAgent/session/format.js +269 -0
  83. package/dist/nativeAgent/session/index.js +5 -0
  84. package/dist/nativeAgent/session/location.js +36 -0
  85. package/dist/nativeAgent/session/sqliteFormat.js +134 -0
  86. package/dist/nativeAgent/session/store.js +248 -0
  87. package/dist/nativeAgent/textTools.js +293 -0
  88. package/dist/nativeAgent/toolManager/executor.js +290 -0
  89. package/dist/nativeAgent/toolManager/index.js +4 -0
  90. package/dist/nativeAgent/validation.js +95 -0
  91. package/dist/plugins/pluginService.js +98 -8
  92. package/dist/resources/projectResourceService.js +23 -1
  93. package/dist/runtime/managedIdentity.js +6 -0
  94. package/dist/surface/surfaceContributions.js +3 -0
  95. package/dist/task/taskAuthority.js +61 -10
  96. package/dist/web/assets/assetManifest.js +38 -22
  97. package/dist/web/assets/client/api.js +115 -0
  98. package/dist/web/assets/client/app.js +615 -862
  99. package/dist/web/assets/client/components.js +323 -897
  100. package/dist/web/assets/client/detail.js +333 -0
  101. package/dist/web/assets/client/dock.js +225 -0
  102. package/dist/web/assets/client/dom.js +55 -3
  103. package/dist/web/assets/client/evidence.js +172 -0
  104. package/dist/web/assets/client/format.js +25 -14
  105. package/dist/web/assets/client/forms.js +230 -0
  106. package/dist/web/assets/client/i18n.js +1024 -791
  107. package/dist/web/assets/client/overview.js +118 -0
  108. package/dist/web/assets/client/records.js +63 -0
  109. package/dist/web/assets/client/sections.js +313 -0
  110. package/dist/web/assets/client/sidebar.js +146 -0
  111. package/dist/web/assets/client/theme.js +61 -22
  112. package/dist/web/assets/icons.js +39 -0
  113. package/dist/web/assets/shell.js +125 -118
  114. package/dist/web/assets/styles/base.js +41 -0
  115. package/dist/web/assets/styles/components.js +169 -0
  116. package/dist/web/assets/styles/layout.js +56 -77
  117. package/dist/web/assets/styles/markdown.js +16 -24
  118. package/dist/web/assets/styles/responsive.js +41 -44
  119. package/dist/web/assets/styles/tokens.js +65 -89
  120. package/dist/web/assets/styles/views.js +292 -0
  121. package/dist/web/webServer.js +2 -1
  122. package/docs/agent-result-consumption.md +16 -0
  123. package/docs/agent-result-consumption.zh-CN.md +13 -0
  124. package/docs/examples/agent-offline.mjs +194 -0
  125. package/docs/native-agent.md +283 -0
  126. package/docs/release-workflow.md +61 -0
  127. package/docs/release-workflow.zh-CN.md +46 -0
  128. package/docs/roles-and-configuration.md +32 -0
  129. package/docs/roles-and-configuration.zh-CN.md +26 -0
  130. package/package.json +1 -1
  131. package/skills/yui-leader/SKILL.md +9 -0
  132. package/skills/yui-reviewer/SKILL.md +5 -0
  133. package/skills/yui-runtime/SKILL.md +33 -0
  134. package/dist/web/assets/client/taskSummary.js +0 -350
  135. package/dist/web/assets/client/taskSurface.js +0 -616
  136. package/dist/web/assets/client/view.js +0 -608
  137. package/dist/web/assets/styles/cards.js +0 -247
  138. package/dist/web/assets/styles/widgets.js +0 -168
package/dist/cli.js CHANGED
@@ -1,3041 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { randomUUID } from "node:crypto";
4
- import { readFileSync, realpathSync } from "node:fs";
5
- import { resolve } from "node:path";
6
- import { createInterface } from "node:readline/promises";
7
- import { fileURLToPath } from "node:url";
8
- import { isDeepStrictEqual } from "node:util";
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";
14
- import { describeCommandTree, findCommandNode } from "./cli/commandCatalog.js";
15
- import { renderCompletion } from "./cli/completion.js";
16
- import { runCompletionWizard } from "./cli/completionWizard.js";
17
- import { resolveCompletionCandidates } from "./cli/dynamicCompletion.js";
18
- import { renderCommandHelp } from "./cli/helpRenderer.js";
19
- import { allowsInteractiveSelection, resolveInteractiveArguments } from "./cli/interactiveSelection.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, ManagedRuntimeDriftError } from "./runtime/managedCaller.js";
24
- import { resolveOperatorWizardArguments } from "./cli/operatorWizard.js";
25
- import { resolveGlobalRoleAgentConfigurationArguments, resolveRoleWizardArguments } from "./cli/roleWizard.js";
26
- import { runUpdateCommand } from "./cli/updateCommand.js";
27
- import { createUpdatePorts } from "./cli/updatePorts.js";
28
- import { runUpgradeCommand } from "./cli/upgradeCommand.js";
29
- import { runAgentCommand } from "./commands/agentCommands.js";
30
- import { runCapabilityCommand } from "./commands/capabilityCommands.js";
31
- import { runConfigCommand } from "./commands/configCommands.js";
32
- import { runConfigOverview } from "./commands/configOverview.js";
33
- import { parseControllerCleanupOptions, parseControllerRuntimeSnapshot, parseControllerStatusOptions, renderControllerResourceStatus, renderRuntimeIdentitySection, runInteractiveControllerCleanup, summarizeDurablePhysicalMismatch } from "./commands/controllerCommands.js";
34
- import { runDurableJobCommand } from "./commands/durableJobCommands.js";
35
- import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
36
- import { runGlobalRoleCommand } from "./commands/globalRoleCommands.js";
37
- import { runJobCommand } from "./commands/jobCommands.js";
38
- import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
39
- import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.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, resolveJobCaller } from "./task/taskAuthority.js";
45
- import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
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 { verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
49
- import { finalizeStoppedTaskExecution, parseTaskExecutionStartRequest, parseTaskExecutionStopRequest, startTaskExecutionCommand, stopTaskExecutionCommand } from "./commands/taskExecutionCommands.js";
50
- import { runTaskIntegrationCommand } from "./commands/taskIntegrationCommands.js";
51
- import { runTaskOverlapCommand } from "./commands/taskOverlapCommands.js";
52
- import { runTaskPublicationAdoptCommand } from "./commands/taskPublicationAdoptCommand.js";
53
- import { runTaskPublicationVerifyCommand } from "./commands/taskPublicationVerifyCommand.js";
54
- import { runTaskUpstreamCommand } from "./commands/taskUpstreamCommands.js";
55
- import { runTaskWorkspaceCommand } from "./commands/taskWorkspaceCommands.js";
56
- import { runTelemetryCommand } from "./commands/telemetryCommands.js";
57
- import { runWorkflowCommandAsync } from "./commands/workflowCommands.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 } from "./context/sessionBootstrapManifest.js";
62
- import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
63
- import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
64
- import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
65
- import { cleanControllerResource } from "./controller/resourceCleanupLinux.js";
66
- import { scanControllerResourceInventory } from "./controller/resourceInventoryLinux.js";
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";
71
- import { buildDoctorReport, renderDoctor, runDoctorCommand } from "./doctor/doctor.js";
72
- import { agentNotFound, CliError, runtimeError, usageError } from "./errors/cliError.js";
73
- import { describeCliFailure, renderCliFailure, sanitizeFailureText, sanitizeFailureDetails } from "./errors/cliFailure.js";
74
- import { AgentConfigurationCatalogService, validateAgentLaunchConfiguration } from "./executor/agentConfigurationCatalog.js";
75
- import { FileRoleLaunchPlanner } from "./executor/fileRoleLaunchPlanner.js";
76
- import { createGitHubCliPublicationVerifier } from "./external/githubPublicationVerifier.js";
77
- import { createGitLabCliPublicationVerifier } from "./external/gitlabPublicationVerifier.js";
78
- import { recordGlobalInterruptResult, recordGlobalSteerResult } from "./message/globalInterrupt.js";
79
- import { collectRuntimeBuildIdentity, collectStorageIdentity, countDroppedInboxEvents, createProductionRuntimeIdentityPorts, evaluateStorageHealth } from "./observability/runtimeIdentity.js";
80
- import { listOperatorSessions, operatorSessionRef } from "./operator/operatorSessionHistory.js";
81
- import { renderAgentConfigurationCatalog } from "./output/agentConfigurationPresentation.js";
82
- import { parseTaskAgentCapabilityQuery } from "./commands/taskAgentCapabilities.js";
83
- import { capabilityReaderIdentity } from "./controller/agentCapabilities.js";
84
- import { formatTimestamp } from "./output/timePresentation.js";
85
- import { resolveAgentProfileView } from "./profile/agentProfileRuntime.js";
86
- import { assertCliHomeReleaseFence } from "./release/cliHomeReleaseFence.js";
87
- import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
88
- import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
89
- import { NodeGitWorkspace } from "./repository/gitWorkspace.js";
90
- import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
91
- import { TaskWorkspaceCoordinator, WorkspaceCleanupBlockedError } from "./repository/taskWorkspaceCoordinator.js";
92
- import { FileTaskWorkspacePreparer } from "./repository/taskWorkspacePreparer.js";
93
- import { snapshotWorkItemCandidate } from "./repository/workItemCandidateSnapshot.js";
94
- import { assessDeltaRecheck } from "./review/deltaRecheck.js";
95
- import { isCompletedTaskReviewEvidence } from "./review/reviewAcceptance.js";
96
- import { createTaskFinalReviewContract, extractTaskFinalReviewRequest } from "./review/taskFinalReviewContract.js";
97
- import { resolveRecordedTaskFinalReviewContract } from "./review/taskFinalReviewContractResolution.js";
98
- import { AGENT_HOST_CONTROL_PROTOCOL, foldInterruptLiveReceipt, foldSteerLiveReceipt, inspectAgentHost, runAgentHost, sendAgentHostAuthorityControl, sendAgentHostCancelControl, sendAgentHostSteerControl } from "./runtime/agentHost.js";
99
- import { unknownAgentRunConfiguration } from "./runtime/agentRunConfiguration.js";
100
- import { isCodexInspectionPermissionError } from "./runtime/codexSharedDaemon.js";
101
- import { prepareLocalCodexStartup } from "./runtime/localStartup.js";
102
- import { requireManagedTaskCaller, resolveManagedTaskReader } from "./runtime/managedCaller.js";
103
- import { assertRuntimeCoherence } from "./runtime/runtimeCoherence.js";
104
- import { runSetupCommand, validateSetupInvocation } from "./setup/setupCommand.js";
105
- import { openCurrentTaskStore, validateCurrentTaskStore } from "./storage/currentTaskStore.js";
106
- import { inspectStorageSchema } from "./storage/storageSchema.js";
107
- import { resolveYuiHome } from "./storage/taskStore.js";
108
- import { renderArchiveDiagnostics, taskArchiveDiagnostics } from "./task/archiveDiagnostics.js";
109
- import { inspectTaskArchive, renderTaskArchivePreflight } from "./task/archivePreflight.js";
110
- import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./task/remoteDeliveryService.js";
111
- import { resolveTaskRecordReference } from "./task/taskRecordReference.js";
112
- import { openSchedulerTelemetry } from "./telemetry/telemetryWiring.js";
113
- import { NodeCommandExecutor } from "./tmux/commandExecutor.js";
114
- import { TmuxManager } from "./tmux/tmuxManager.js";
115
- import { YUI_VERSION, yuiVersionIdentity } from "./version.js";
116
- import { parseWebCommandOptions } from "./web/webServer.js";
117
- import { currentWorkItemExecutionGroup } from "./workItem/workItem.js";
118
- import { CleanupInspectionError } from "./workspace/cleanupInspection.js";
119
- import { WorkItemChangeSetManager } from "./workspace/workItemChangeSetManager.js";
120
- import { workspaceProjectEntry } from "./worktree/managedWorkspace.js";
121
- const VERSION = YUI_VERSION;
122
- const taskFinalReviewInvocation = extractTaskFinalReviewRequest(process.argv.slice(2));
123
- const rawArgs = [...taskFinalReviewInvocation.args];
124
- const jsonOutput = rawArgs.includes("--json");
125
- const args = normalizeAliases(jsonOutput ? rawArgs.filter((argument) => argument !== "--json") : rawArgs);
126
- let invocationHome;
127
- void main().catch((error) => {
128
- if (error instanceof CleanupInspectionError) {
129
- error = cleanupCliError(error, error.checks[0]?.resource ?? "workspace");
130
- }
131
- const failure = describeCliFailure(error, failureContext());
132
- process.stderr.write(`${renderCliFailure(failure, jsonOutput)}\n`);
133
- process.exitCode = failure.exitCode;
134
- });
135
- function failureContext() {
136
- const route = routeInvocation(args);
137
- const node = "node" in route ? route.node : route.helpNode;
138
- const operands = args.slice(node.path.length - 1);
139
- const publicValues = new Set([
140
- ...Object.values(node.argumentValues).flat(),
141
- ...Object.values(node.optionValues).flat()
142
- ]);
143
- const recordId = /^(?:task|project|message|run|work|wake|operation|review|input|job|integration|publication)-\d+(?:\/(?:message|run|work|wake|review|input|job|integration|publication)-\d+)?$/;
144
- return {
145
- operation: node.path.join(" "),
146
- targets: operands.filter(value => recordId.test(value)),
147
- // Do not replay argv: operands include Message bodies, config values and credentials.
148
- privateValues: operands.filter(value => !/^--[a-z][a-z-]*$/.test(value) && !recordId.test(value) && !publicValues.has(value)),
149
- home: invocationHome ?? process.env.YUI_HOME ?? "(default Home unresolved)",
150
- cli: fileURLToPath(import.meta.url),
151
- environment: process.env
152
- };
153
- }
154
- export async function main() {
155
- const home = resolveYuiHome(process.env);
156
- invocationHome = home;
157
- const routedForFence = args.length === 0 ? undefined : routeInvocation(args);
158
- const managedInvocation = process.env.YUI_SESSION_SCOPE === "task"
159
- || process.env.YUI_SESSION_SCOPE === "global";
160
- const homeFreeInvocation = args.length === 0
161
- || (args[0] === "version" && args.length === 1)
162
- || routedForFence?.kind === "help"
163
- || routedForFence?.kind === "path-error"
164
- || routedForFence?.kind === "incomplete";
165
- if (!homeFreeInvocation) {
166
- assertCliHomeReleaseFence({
167
- home,
168
- packageRoot: fileURLToPath(new URL("../", import.meta.url)),
169
- entryPath: fileURLToPath(import.meta.url),
170
- args
171
- });
172
- }
173
- const delegatedDriver = explicitReleaseActivationDriver();
174
- if (delegatedDriver !== null) {
175
- const delegated = spawnSync(process.execPath, [delegatedDriver, ...process.argv.slice(2)], {
176
- cwd: process.cwd(),
177
- env: process.env,
178
- stdio: "inherit"
179
- });
180
- if (delegated.error !== undefined) {
181
- throw runtimeError("Target release activation driver could not start.", { cause: delegated.error });
182
- }
183
- if (delegated.signal !== null) {
184
- throw runtimeError(`Target release activation driver stopped by signal ${delegated.signal}.`);
185
- }
186
- process.exitCode = delegated.status ?? 5;
187
- return;
188
- }
189
- if (args.length === 0) {
190
- emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
191
- return;
192
- }
193
- if (args[0] === "version" && args.length === 1) {
194
- emit(VERSION, true, yuiVersionIdentity());
195
- return;
196
- }
197
- const invocation = routeInvocation(args);
198
- if (invocation.kind === "help") {
199
- emit(renderCommandHelp(invocation.node, VERSION), true);
200
- return;
201
- }
202
- if (invocation.kind === "path-error") {
203
- throw usageError(`Unknown command: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
204
- }
205
- if (invocation.kind === "incomplete") {
206
- throw usageError(`Command required after: ${invocation.typedPath}`, renderCommandHelp(invocation.helpNode, VERSION));
207
- }
208
- if (args[0] === "version")
209
- throw usageError("Version usage: yui version");
210
- const { contract: taskFinalReviewContract, verifiedStore } = await preflightManagedTaskControlPlane();
211
- assertTaskInvocationScope(args, process.env);
212
- if (["config", "resources"].includes(args[0] ?? "") && (managedInvocation
213
- || process.env.YUI_ROLE !== undefined || process.env.YUI_AGENT_ID !== undefined
214
- || process.env.YUI_NATIVE_SESSION_ID !== undefined)) {
215
- const ownedStore = verifiedStore === undefined ? openCurrentTaskStore(home) : undefined;
216
- try {
217
- assertConfigurationAuthority(args, verifiedStore ?? ownedStore, process.env);
218
- }
219
- finally {
220
- ownedStore?.close();
221
- }
222
- }
223
- if (args[0] === "update") {
224
- if (jsonOutput)
225
- throw usageError("Update does not support --json.");
226
- process.exitCode = runUpdateCommand(args.slice(1));
227
- return;
228
- }
229
- if (args[0] === "release") {
230
- const subcommand = args[1];
231
- if (subcommand === "install" && args.length === 3) {
232
- const result = runReleaseInstall(home, args[2]);
233
- if (result.outcome === "aborted")
234
- emitFailureResult(renderReleaseInstallResult(result), result);
235
- else
236
- emit(renderReleaseInstallResult(result), false, result);
237
- if (result.outcome === "aborted")
238
- process.exitCode = 5;
239
- return;
240
- }
241
- if (subcommand === "list" && args.length === 2) {
242
- const result = runReleaseList(home);
243
- emit(renderReleaseList(result), false, result);
244
- return;
245
- }
246
- if (subcommand === "activate" && (args.length === 2 || args.length === 3)) {
247
- const releaseId = args.length === 3
248
- ? args[2]
249
- : runReleaseList(home).active ?? undefined;
250
- if (releaseId === undefined) {
251
- throw usageError("Release activate usage: yui release activate <release-id> "
252
- + "(or activate the active release when exactly one is installed).");
253
- }
254
- const result = await runReleaseActivate(home, releaseId);
255
- if (result.outcome === "aborted" || result.outcome === "dual-owner") {
256
- emitFailureResult(renderReleaseActivateResult(result), result);
257
- }
258
- else
259
- emit(renderReleaseActivateResult(result), false, result);
260
- if (result.outcome === "aborted" || result.outcome === "dual-owner") {
261
- process.exitCode = 5;
262
- }
263
- return;
264
- }
265
- throw usageError("Release usage: yui release install <source-dir> | list | activate [release-id].");
266
- }
267
- if (args[0] === "config" && args[1] === "completion") {
268
- await completionCommand(home, invocation.node);
269
- return;
270
- }
271
- if (args[0] === "config" && args[1] === "describe") {
272
- const describeNode = findCommandNode(["config", "describe"]);
273
- if (describeNode === undefined)
274
- throw new Error("Config describe command is missing from the catalog.");
275
- const domain = args[2];
276
- const domains = describeNode.argumentValues[0] ?? [];
277
- if (args.length > 3 || (domain !== undefined && !domains.includes(domain))) {
278
- throw usageError(`Config describe usage: ${describeNode.usage.join(" | ")}.`);
279
- }
280
- const target = findCommandNode(domain === undefined ? ["config"] : ["config", domain]);
281
- if (target === undefined)
282
- throw new Error(`Config domain is missing from the catalog: ${domain}.`);
283
- emit(renderCommandHelp(target, VERSION), false, describeCommandTree(target));
284
- return;
285
- }
286
- if (args[0] === "capability") {
287
- const result = await runCapabilityCommand(args.slice(1), home, process.env);
288
- emit(JSON.stringify(result, null, 2), false, result);
289
- const kind = result?.kind;
290
- if (typeof result === "object" && result !== null && !Array.isArray(result)
291
- && typeof kind === "string" && !["value", "operation"].includes(kind))
292
- process.exitCode = 5;
293
- return;
294
- }
295
- if (args[0] === "setup") {
296
- if (jsonOutput)
297
- throw usageError("Setup does not support --json.");
298
- await assertFileTaskControllerStorageCompatible(home);
299
- const setupIo = {
300
- input: process.stdin,
301
- output: process.stdout,
302
- forceInteractive: process.env.YUI_SETUP_INTERACTIVE === "1"
303
- };
304
- validateSetupInvocation(args.slice(1), setupIo);
305
- const output = await runSetupCommand(args.slice(1), process.env, new NodeCommandExecutor(), setupIo);
306
- // A successful setup leaves the Home ready for normal Yui work. Start the
307
- // detached per-Home Controller even when setup began with no Controller;
308
- // read-only commands and failed setup still remain non-starting paths.
309
- await ensureFileTaskController(home, { environment: process.env });
310
- const setupStore = openCurrentTaskStore(home);
311
- try {
312
- await prepareCodexForCurrentRoles(setupStore, home);
313
- }
314
- finally {
315
- setupStore.close();
316
- }
317
- const refresh = await refreshRunningFileTaskControllerEnvironment(home, openCurrentTaskStore(home), process.env);
318
- emit(withControllerRefreshWarning(output, refresh, "Agent environment"));
319
- return;
320
- }
321
- if (args[0] === "doctor") {
322
- const doctorArgs = args.slice(1);
323
- if (doctorArgs.length !== 0) {
324
- // Preserve the usage error for stray operands (parity with text mode).
325
- await runDoctorCommand(doctorArgs, process.env, new NodeCommandExecutor());
326
- return;
327
- }
328
- const report = await buildDoctorReport(process.env, new NodeCommandExecutor());
329
- if (jsonOutput) {
330
- // Machine-readable result: the full checks array + a storage-health verdict
331
- // the update post-verify parses (P1-3). Exit non-zero when storage is not
332
- // healthy so even a naive exit-code check fails closed. This exit-code
333
- // signal is scoped to the --json path; text-mode doctor keeps its existing
334
- // presentation and exit 0 (the WorkItem allows doctor's presentation to stay).
335
- if (!report.storage.healthy)
336
- process.exitCode = 5;
337
- emit("", false, report);
338
- return;
339
- }
340
- emit(renderDoctor(report.checks, report.review));
341
- return;
342
- }
343
- if (args[0] === "start") {
344
- if (args.length !== 1)
345
- throw usageError("Start usage: yui start.");
346
- validateCurrentTaskStore(home);
347
- const startupStore = openCurrentTaskStore(home);
348
- try {
349
- assertHomeStartupAuthority(startupStore);
350
- const controller = await ensureFileTaskController(home, { environment: process.env });
351
- let codex;
352
- try {
353
- codex = await prepareLocalCodexStartup(startupStore, home, process.env);
354
- }
355
- catch (error) {
356
- throw runtimeError("Controller is ready, but Codex shared startup failed.", { cause: error });
357
- }
358
- const lines = ["Yui Controller is ready."];
359
- if (codex.daemons.length === 0)
360
- lines.push("No active Role needs Codex; no Codex daemon was started.");
361
- else
362
- for (const daemon of codex.daemons) {
363
- lines.push(`Codex shared daemon ${daemon.started ? "started" : "ready"}: ${daemon.socketPath}`);
364
- }
365
- if (codex.app === "restart-required") {
366
- lines.push("Codex App is running with an older connection. Quit and reopen it to use Yui sessions.");
367
- }
368
- else if (codex.app === "ready") {
369
- lines.push("Codex App launch is configured for the shared daemon. Use yui doctor to verify its live connection.");
370
- }
371
- else if (codex.app === "multiple-homes") {
372
- lines.push("Active Codex Roles use multiple Codex homes; App sharing needs one selected Home.");
373
- }
374
- else if (codex.app === "not-detected") {
375
- lines.push("Codex App was not detected; the shared daemon is ready for Codex CLI sessions.");
376
- }
377
- emit(lines.join("\n"), false, { controller, codex });
378
- }
379
- finally {
380
- startupStore.close();
381
- }
382
- return;
383
- }
384
- if (args[0] === "execution") {
385
- // Issue 11 read-only audit: opens the Home read-only, never writes state,
386
- // never wakes a Leader.
387
- if (args[1] !== "audit") {
388
- throw usageError("Execution usage: yui execution audit [--task <id>] [--since <iso>] [--until <iso>].");
389
- }
390
- const options = parseExecutionAuditOptions(args.slice(2));
391
- const result = runExecutionAuditCommand(home, options);
392
- emit(result.output, false, result.report);
393
- return;
394
- }
395
- if (args[0] === "upgrade") {
396
- const result = await runUpgradeCommand(args.slice(1), home);
397
- process.exitCode = result.exitCode;
398
- if (result.exitCode !== 0)
399
- emitFailureResult(result.output, result.data);
400
- else
401
- emit(result.output, false, result.data);
402
- return;
403
- }
404
- if (args[0] === "internal") {
405
- if (args[1] === "agent-host" && args.length === 3) {
406
- process.exitCode = await runAgentHost({
407
- home,
408
- ticket: args[2]
409
- });
410
- return;
411
- }
412
- if (args[1] === "session-notify" && args.length === 3) {
413
- await runSessionNotifyCommand(args[2], process.env);
414
- return;
415
- }
416
- if (args[1] === "runtime-hook" && args.length === 2) {
417
- // Provider lifecycle Hooks are observation channels, never execution
418
- // gates. A late/stale Hook must not make Claude reject an otherwise
419
- // valid Session or AgentRun; its exact fence is revalidated before any
420
- // inbox fact is written, so dropping an invalid observation is safe.
421
- try {
422
- await runRuntimeObservationHookCommand(readFileSync(0, "utf8"), process.env);
423
- }
424
- catch {
425
- return;
426
- }
427
- return;
428
- }
429
- throw usageError("Internal lifecycle callback usage is invalid.");
430
- }
431
- if (args[0] === "session" && args[1] === "reconcile") {
432
- const options = parseSessionReconcileOptions(args.slice(2));
433
- const store = openCurrentTaskStore(home);
434
- const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), new NodeCommandExecutor(), {
435
- yuiHome: home,
436
- historyLimit: resolveTmuxHistoryLimit(store.getConfig().tmuxHistoryLimit)
437
- });
438
- const reconciliation = new SessionOwnerReconciliation({
439
- home,
440
- store,
441
- environment: process.env,
442
- tmux
443
- });
444
- const result = await runSessionReconcileCommand({
445
- reconciliation,
446
- options,
447
- environment: process.env
448
- });
449
- process.exitCode = result.exitCode;
450
- emit(result.output, false, result.data);
451
- return;
452
- }
453
- if (args[0] === "controller") {
454
- const method = args[1];
455
- if (method === "start" && args.length === 2) {
456
- validateCurrentTaskStore(home);
457
- const startupStore = openCurrentTaskStore(home);
458
- try {
459
- assertHomeStartupAuthority(startupStore);
460
- }
461
- finally {
462
- startupStore.close();
463
- }
464
- const result = await ensureFileTaskController(home, { environment: process.env });
465
- emit("Controller is ready.", false, result);
466
- return;
467
- }
468
- if (method === "live-identity" && args.length === 2) {
469
- try {
470
- const identity = await callController(home, "controller.identity", {});
471
- emit("", false, identity);
472
- }
473
- catch (error) {
474
- if (!(error instanceof ControllerClientError))
475
- throw error;
476
- process.stderr.write(`${renderCliFailure(describeCliFailure(error, failureContext()), jsonOutput, error.code)}\n`);
477
- process.exitCode = 5;
478
- }
479
- return;
480
- }
481
- if (method === "identity" && args.length === 2) {
482
- // The receipt survives a Controller stop. If it has never been written,
483
- // ask the authenticated live Controller for the same current identity.
484
- const receipt = readRuntimeIdentity(home);
485
- if (receipt !== null) {
486
- emit("", false, receipt);
487
- return;
488
- }
489
- try {
490
- const identity = await callController(home, "controller.identity", {});
491
- emit("", false, identity);
492
- }
493
- catch (error) {
494
- // Preserve the Controller protocol code for the synchronous update
495
- // lifecycle owner. A generic RUNTIME_ERROR would erase the only
496
- // definitive CONTROLLER_NOT_RUNNING proof and force an unnecessary
497
- // unknown-active block.
498
- if (!(error instanceof ControllerClientError))
499
- throw error;
500
- process.stderr.write(`${renderCliFailure(describeCliFailure(error, failureContext()), jsonOutput, error.code)}\n`);
501
- process.exitCode = 5;
502
- }
503
- return;
504
- }
505
- if (method === "status") {
506
- const options = parseControllerStatusOptions(args.slice(2));
507
- const snapshot = await scanControllerResourceInventory({
508
- currentHome: home,
509
- scope: options.scope,
510
- environment: process.env
511
- });
512
- // Status always includes identity/health. Missing producers stay
513
- // `unsupported`; storage contradictions retain the nonzero health exit.
514
- const cliEntry = fileURLToPath(import.meta.url);
515
- const packageRoot = resolve(cliEntry, "..", "..");
516
- const build = collectRuntimeBuildIdentity(createProductionRuntimeIdentityPorts(packageRoot, cliEntry, process.env));
517
- const storage = collectStorageIdentity(home);
518
- const droppedEvents = countDroppedInboxEvents(home);
519
- let runtime;
520
- try {
521
- const result = await callController(home, "controller.status", {}, { timeoutMs: 2_000 });
522
- runtime = parseControllerRuntimeSnapshot(result, droppedEvents);
523
- }
524
- catch {
525
- runtime = { source: "unsupported", droppedEvents };
526
- }
527
- const mismatch = summarizeDurablePhysicalMismatch(snapshot);
528
- const identitySection = renderRuntimeIdentitySection({
529
- build,
530
- storage,
531
- runtime,
532
- mismatch,
533
- inventoryRssBytes: snapshot.summary.rssBytes
534
- });
535
- emit(`${renderControllerResourceStatus(snapshot, options.verbose)}\n\n${identitySection}`, false, { ...snapshot, identity: { build, storage, runtime, mismatch } });
536
- if (evaluateStorageHealth(storage).status === "fail")
537
- process.exitCode = 5;
538
- return;
539
- }
540
- if (method === "cleanup") {
541
- if (jsonOutput)
542
- throw usageError("Controller cleanup does not support --json.");
543
- const options = parseControllerCleanupOptions(args.slice(2));
544
- const readline = createInterface({
545
- input: process.stdin,
546
- output: process.stdout
547
- });
548
- try {
549
- const result = await runInteractiveControllerCleanup({
550
- io: {
551
- interactive: process.stdin.isTTY === true && process.stdout.isTTY === true,
552
- write: (value) => process.stdout.write(value),
553
- question: async (prompt) => readline.question(prompt)
554
- },
555
- scan: () => scanControllerResourceInventory({
556
- currentHome: home,
557
- scope: options.scope,
558
- environment: process.env
559
- }),
560
- clean: (resource) => cleanControllerResource(resource, {
561
- environment: process.env
562
- })
563
- });
564
- if (result.data.failed.length > 0 || result.data.skipped.length > 0) {
565
- process.exitCode = 5;
566
- }
567
- emit(result.output, false, result.data);
568
- }
569
- finally {
570
- readline.close();
571
- }
572
- return;
573
- }
574
- if ((method !== "stop" && method !== "restart") || args.length !== 2) {
575
- throw usageError("Controller usage: yui controller status [--all] [--verbose] | "
576
- + "cleanup [--all] | start | stop | restart.");
577
- }
578
- // Stopping an exactly identified Controller is a recovery operation:
579
- // malformed domain records must not prevent quiescing its process.
580
- if (method === "restart")
581
- validateCurrentTaskStore(home);
582
- const controllerMethod = method;
583
- const updateHandoverOwner = process.env.YUI_UPDATE_HANDOVER_OWNER_PID;
584
- // Maintenance children must explicitly name their exact lock owner.
585
- const updateHandoverOwnerPid = updateHandoverOwner === undefined
586
- ? undefined
587
- : Number(updateHandoverOwner);
588
- if (updateHandoverOwnerPid !== undefined
589
- && (!Number.isSafeInteger(updateHandoverOwnerPid) || updateHandoverOwnerPid < 1)) {
590
- throw runtimeError("Update Controller handover owner PID is invalid.");
591
- }
592
- const controllerOptions = {
593
- environment: process.env,
594
- ...(updateHandoverOwnerPid === undefined ? {} : { handoverOwnerPid: updateHandoverOwnerPid })
595
- };
596
- const result = controllerMethod === "restart"
597
- ? await restartFileTaskController(home, controllerOptions)
598
- : await stopFileTaskController(home, controllerOptions);
599
- // The update lifecycle needs the authenticated replacement PID returned by
600
- // restart/readiness. Keep stop's long-standing text envelope, while
601
- // exposing restart's structured result alongside its human output.
602
- emit(renderControllerResult(controllerMethod, result), false, controllerMethod === "restart" ? result : undefined);
603
- return;
604
- }
605
- if (args[0] === "resources") {
606
- await assertFileTaskControllerStorageCompatible(home);
607
- const resourcesStore = openCurrentTaskStore(home);
608
- const result = await runResourcesCommand(args.slice(1), resourcesStore);
609
- emit(result.output, false, result.data);
610
- return;
611
- }
612
- if (!operatorOfflineCommand(args))
613
- await assertFileTaskControllerStorageCompatible(home);
614
- // Reuse the store the exact runtime preflight already opened and read for
615
- // this same Home. Opening a second store would parse the unchanged large
616
- // state a second time; the per-instance fingerprint cache still invalidates
617
- // on an external writer, and the storage lock + revision CAS are unchanged.
618
- const store = verifiedStore !== undefined
619
- && resolve(verifiedStore.rootDirectory()) === resolve(home)
620
- ? verifiedStore
621
- : openCurrentTaskStore(home);
622
- const catalogs = new AgentConfigurationCatalogService(home, {
623
- environment: process.env
624
- });
625
- const resolved = await resolveTerminalArguments(args, invocation.node, store, catalogs);
626
- if (resolved === null) {
627
- emit("Cancelled.");
628
- return;
629
- }
630
- assertTaskInvocationScope(resolved, process.env);
631
- if (resolved[0] === "task" && resolved[1] === "role" && resolved[2] === "capabilities") {
632
- const query = parseTaskAgentCapabilityQuery(resolved.slice(3));
633
- const result = await callController(home, "task.role-capabilities", {
634
- ...query, reader: capabilityReaderIdentity(process.env)
635
- }, { timeoutMs: 15_000 });
636
- emit(`Configuration: ${result.context.selection} ${result.context.taskId}/${result.context.roleName}\n`
637
- + "Discovery uses the Controller's current native account environment; settings files and secret values are not historical snapshots.\n"
638
- + renderAgentConfigurationCatalog(result), false, result);
639
- return;
640
- }
641
- const validateAgentConfiguration = await preflightAgentConfigurationMutation(resolved, store, catalogs);
642
- const executor = new NodeCommandExecutor();
643
- const tmux = new TmuxManager(resolveTmuxBin(store.getConfig().tmuxBin), executor, {
644
- yuiHome: home,
645
- historyLimit: resolveTmuxHistoryLimit(store.getConfig().tmuxHistoryLimit),
646
- terminalInput: process.stdin,
647
- onWarning: (message) => writeCliWarning(message)
648
- });
649
- const telemetry = openSchedulerTelemetry(home, store.getConfig());
650
- try {
651
- const schedulerStore = new FileSchedulerStoreAdapter(store, telemetry);
652
- const planner = new FileRoleLaunchPlanner(home, store, { environment: process.env });
653
- const workspacePreparer = new FileTaskWorkspacePreparer(home, store);
654
- const runtime = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
655
- environment: process.env,
656
- onError: (error) => {
657
- // Advisory async failure: preserve the foreground command's exit status.
658
- const failure = describeCliFailure(error, failureContext());
659
- process.stderr.write(`${renderCliFailure({
660
- ...failure, details: { ...failure.details, advisory: true }
661
- }, jsonOutput)}\n`);
662
- }
663
- });
664
- const workspaceCoordinator = new TaskWorkspaceCoordinator(store, workspacePreparer, runtime);
665
- if (resolved[0] === "web") {
666
- if (managedInvocation || process.env.YUI_ROLE || process.env.YUI_NATIVE_SESSION_ID) {
667
- throw usageError("The Web user ingress must be started from a local user terminal, not a managed Session.");
668
- }
669
- if (resolved.length === 2 && (resolved[1] === "--status" || resolved[1] === "--stop")) {
670
- const status = await callController(home, "web.status", {});
671
- if (resolved[1] === "--status")
672
- emit(status ? `Yui web control room: ${status.url}\n` : "Web is not running.\n", false, status);
673
- else {
674
- if (status)
675
- await callController(home, "web.stop", { id: status.id });
676
- emit("Web listener stopped; Controller and Agents are unchanged.\n");
677
- }
678
- return;
679
- }
680
- if (jsonOutput)
681
- throw usageError("Web start does not support --json; use --status.");
682
- const options = parseWebCommandOptions(resolved.slice(1));
683
- const id = randomUUID();
684
- await callController(home, "web.start", { ...options, id });
685
- const displayHost = options.host === "::1" ? "[::1]" : options.host;
686
- process.stdout.write(`Yui web control room: http://${displayHost}:${options.port}\n`);
687
- await new Promise((resolve) => {
688
- const stop = () => {
689
- process.off("SIGINT", stop);
690
- process.off("SIGTERM", stop);
691
- resolve();
692
- };
693
- process.once("SIGINT", stop);
694
- process.once("SIGTERM", stop);
695
- });
696
- await callController(home, "web.stop", { id });
697
- return;
698
- }
699
- if (resolved[0] === "config") {
700
- const domain = resolved[1];
701
- const roleOptions = {
702
- yuiHome: home,
703
- env: process.env
704
- };
705
- if (domain === "show") {
706
- const result = runConfigOverview(resolved.slice(2), store, process.env, roleOptions);
707
- emit(result.output, false, result.data);
708
- return;
709
- }
710
- if (CONFIG_DOMAINS.includes(domain ?? "")) {
711
- const configDomain = domain;
712
- const domainArgs = resolved.slice(2);
713
- const result = runConfigCommand(configDomain, domainArgs, store);
714
- if (domainArgs[0] === "set"
715
- && domainArgs[1] === "reconciliation-interval-seconds") {
716
- const refresh = await refreshRunningFileTaskControllerConfiguration(home, { environment: process.env });
717
- emit(withControllerRefreshWarning(result.output, refresh, "Controller configuration"));
718
- return;
719
- }
720
- emit(result.output, false, result.data);
721
- return;
722
- }
723
- if (domain === "agent") {
724
- const agentArgs = resolved.slice(2);
725
- if (agentArgs[0] === "capabilities") {
726
- if (agentArgs.length !== 2 && !(agentArgs.length === 3 && agentArgs[2] === "--refresh")) {
727
- throw usageError("Agent capabilities usage: yui config agent capabilities <agent-id> [--refresh]");
728
- }
729
- const agent = store.getConfiguredAgent(agentArgs[1] ?? "");
730
- if (agent === null)
731
- throw agentNotFound(agentArgs[1] ?? "");
732
- const result = await catalogs.resolve({
733
- agent,
734
- cwd: store.getConfig().defaultWorkspace ?? process.cwd(),
735
- refresh: agentArgs[2] === "--refresh"
736
- });
737
- emit(renderAgentConfigurationCatalog(result), false, result);
738
- return;
739
- }
740
- const affectedAgentId = agentArgs[1];
741
- const previousAgent = typeof affectedAgentId === "string"
742
- ? store.getConfiguredAgent(affectedAgentId)
743
- : null;
744
- const output = runAgentCommand(agentArgs, store);
745
- if (agentArgs[0] === "add"
746
- || agentArgs[0] === "update"
747
- || agentArgs[0] === "remove") {
748
- const currentAgent = typeof affectedAgentId === "string"
749
- ? store.getConfiguredAgent(affectedAgentId)
750
- : null;
751
- const capabilityNotice = currentAgent !== null
752
- && (agentArgs[0] === "add" || agentArgs[0] === "update")
753
- ? renderAgentConfigurationResolutionNotice(await catalogs.resolve({
754
- agent: currentAgent,
755
- cwd: store.getConfig().defaultWorkspace ?? process.cwd()
756
- }))
757
- : "";
758
- const scope = agentEnvironmentRefreshScope(previousAgent, currentAgent, store.listConfiguredAgents());
759
- const refresh = await refreshRunningFileTaskControllerEnvironment(home, store, process.env, scope);
760
- emit(withControllerRefreshWarning(`${output.trimEnd()}${capabilityNotice.length === 0 ? "\n" : `\n${capabilityNotice}`}`, refresh, "Agent environment"));
761
- return;
762
- }
763
- emit(output);
764
- return;
765
- }
766
- if (domain === "profile") {
767
- const result = runProfileCommand(resolved.slice(2), store, () => new Date(), validateAgentConfiguration === undefined ? {} : { validateAgentConfiguration });
768
- emit(result.output, false, result.data);
769
- return;
770
- }
771
- if (domain === "role") {
772
- const result = runGlobalRoleCommand(resolved.slice(2), store, roleOptions);
773
- if (typeof result !== "string") {
774
- throw new Error("Config Role commands cannot enter a runtime Session.");
775
- }
776
- emit(result);
777
- return;
778
- }
779
- throw usageError(`Unknown configuration domain: ${domain ?? ""}.`);
780
- }
781
- if (resolved[0] === "project") {
782
- const result = await runProjectCommand(resolved.slice(1), store, { environment: process.env });
783
- emit(result.output, false, result.data);
784
- return;
785
- }
786
- if (resolved[0] === "session") {
787
- if (resolved[1] === "stop") {
788
- const options = parseSessionStopOptions(resolved.slice(2));
789
- const result = await runSessionStopCommand({
790
- options,
791
- runtime: {
792
- beginMaintenance: () => acquireHandoverLock(home),
793
- snapshot: () => ({
794
- candidates: schedulerStore.listRuntimeSessionCandidates(),
795
- dormant: schedulerStore.listDormantRuntimeOwners()
796
- }),
797
- drainController: () => runtime.drainController(),
798
- stopController: () => stopFileTaskController(home, {
799
- environment: process.env
800
- }),
801
- startController: async () => {
802
- await ensureFileTaskController(home, { environment: process.env });
803
- },
804
- stopDormantSession: (candidate) => runtime.stopDormantSession(candidate)
805
- },
806
- environment: process.env
807
- });
808
- process.exitCode = result.exitCode;
809
- emit(result.output, false, result.data);
810
- return;
811
- }
812
- const roleOptions = {
813
- yuiHome: home,
814
- env: process.env,
815
- jsonOutput
816
- };
817
- const sessionArgs = resolved[1] === "enter" || resolved[1] === "context"
818
- ? [resolved[1], ...resolved.slice(2)]
819
- : ["session", resolved[1] ?? "", ...resolved.slice(2)];
820
- const result = runGlobalRoleCommand(sessionArgs, store, roleOptions);
821
- if (typeof result !== "string" && result.kind === "output") {
822
- emit(result.output, false, result.data);
823
- return;
824
- }
825
- if (typeof result === "string") {
826
- emit(result);
827
- return;
828
- }
829
- // The session surface (record/replace/enter/context) only ever yields the
830
- // enter control; the live input actions live under the top-level `role`
831
- // command below, never here.
832
- if (result.kind !== "enter") {
833
- throw new Error("Session commands cannot perform a live input control.");
834
- }
835
- await prepareInteractiveCodexApp(result.role.activeAgentId, store, home);
836
- await ensureFileTaskController(home, { environment: process.env });
837
- await runtime.prepareGlobalRoleEnter(result.role.name);
838
- tmux.attachRole("operator", result.role.name, "auto");
839
- return;
840
- }
841
- if (resolved[0] === "role") {
842
- // decision-3 §7 CLI grammar: the durable Global input actions are top-level
843
- // `role message queue|steer <global-role> …` and `role interrupt
844
- // <global-role> …`, distinct from `config role …` (desired configuration)
845
- // and `session …` (native session lifecycle). Core persists the durable
846
- // Global-owned Message, proves owner/target/capability/writer-fence from
847
- // durable state, and returns either a string disposition (queued,
848
- // idempotent-replay, or an explicit not-steered/not-interrupted failure with
849
- // its exact code) or a resolved live intent. The CLI performs at most one
850
- // native edge with scope "global" and taskId omitted — the same shared
851
- // resolver and transport as a Task Role, never a fabricated Task.
852
- let globalInputFailure;
853
- const roleOptions = {
854
- yuiHome: home,
855
- env: process.env,
856
- jsonOutput,
857
- onInputFailure: failure => { globalInputFailure = failure; }
858
- };
859
- const result = runGlobalRoleCommand(resolved.slice(1), store, roleOptions);
860
- if (resolved[1] === "message" && resolved[2] === "queue" && resolved[3] !== undefined) {
861
- await callController(home, "scheduler.signal", {
862
- key: `global-role:${encodeURIComponent(resolved[3])}`
863
- }).catch(() => { });
864
- }
865
- if (typeof result === "string") {
866
- if (globalInputFailure !== undefined) {
867
- emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
868
- return;
869
- }
870
- emit(result);
871
- return;
872
- }
873
- if (result.kind === "output") {
874
- if (globalInputFailure !== undefined) {
875
- emitControlFailure(globalInputFailure.detail, globalInputFailure.code, globalInputFailure.data);
876
- }
877
- else
878
- emit(result.output, false, result.data);
879
- return;
880
- }
881
- if (result.kind === "input-steer") {
882
- // Core already persisted the durable Global Message and proved target +
883
- // capability + writer fence from the Global Role's own Session set. This
884
- // is the single live edge: one native steer of the exact current Turn,
885
- // scope "global", no retarget and no fallback to interrupt or queue.
886
- await ensureFileTaskController(home, { environment: process.env });
887
- let control;
888
- try {
889
- control = await sendAgentHostSteerControl({
890
- home,
891
- scope: "global",
892
- roleName: result.roleName,
893
- control: {
894
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
895
- type: "steer-turn",
896
- nativeSessionId: result.target.nativeSessionId,
897
- nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
898
- authority: result.target.authority,
899
- run: { attemptId: result.receiptId, boundedText: result.text }
900
- }
901
- });
902
- }
903
- catch (error) {
904
- recordGlobalSteerResult(store, result.roleName, result.messageId, {
905
- state: "steer-unknown", outcome: "pending",
906
- detail: error instanceof Error ? error.message : String(error)
907
- });
908
- throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
909
- + `${error instanceof Error ? error.message : String(error)}. `
910
- + "The Message is retained and its outcome is recorded from the Host; whether the "
911
- + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
912
- + "do not reissue the same input under a new requestId or a different action.");
913
- }
914
- const steer = foldSteerLiveReceipt(control);
915
- recordGlobalSteerResult(store, result.roleName, result.messageId, steer);
916
- if (steer.state !== "steered") {
917
- 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 });
918
- return;
919
- }
920
- emit(steerReceiptOutput(result.output, result.roleName, result.messageId, steer), false, {
921
- roleName: result.roleName,
922
- messageId: result.messageId, steer
923
- });
924
- return;
925
- }
926
- if (result.kind === "input-interrupt") {
927
- // The single live edge for a Global interrupt: one native cancel of the
928
- // exact current Turn, scope "global". Never a kill/restart/detach. Any
929
- // then-handoff was already claimed durably by Core (an existing Global
930
- // Message owned by this Role) and is delivered once by the ordinary
931
- // continuation path after this Turn reaches a proven terminal.
932
- await ensureFileTaskController(home, { environment: process.env });
933
- let control;
934
- try {
935
- control = await sendAgentHostCancelControl({
936
- home,
937
- scope: "global",
938
- roleName: result.roleName,
939
- control: {
940
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
941
- type: "cancel",
942
- nativeOnly: true,
943
- nativeSessionId: result.target.nativeSessionId,
944
- attemptId: result.target.attemptId,
945
- authority: result.target.authority
946
- }
947
- });
948
- }
949
- catch (error) {
950
- recordGlobalInterruptResult(store, result.roleName, result.receiptId, {
951
- state: "interrupt-unknown", outcome: "cancel-requested",
952
- detail: error instanceof Error ? error.message : String(error)
953
- });
954
- throw runtimeError(`Interrupt of global role ${result.roleName} did not complete: `
955
- + `${error instanceof Error ? error.message : String(error)}. `
956
- + "No process was killed; re-read the Session before retrying.");
957
- }
958
- const interrupt = foldInterruptLiveReceipt(control);
959
- recordGlobalInterruptResult(store, result.roleName, result.receiptId, interrupt);
960
- if (interrupt.state !== "interrupt-requested") {
961
- emitControlFailure(interruptReceiptOutput(result.output, result.roleName, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN" : "INTERRUPT_NOT_DELIVERED", { roleName: result.roleName, interrupt });
962
- return;
963
- }
964
- emit(interruptReceiptOutput(result.output, result.roleName, interrupt), false, {
965
- roleName: result.roleName,
966
- ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
967
- interrupt
968
- });
969
- return;
970
- }
971
- if (result.kind !== "enter") {
972
- throw new Error("Role command returned an invalid control result.");
973
- }
974
- // A top-level `role` command never enters a runtime Session; that is
975
- // `session enter`.
976
- throw usageError("Use 'yui session enter <role>' to attach to a Global Role.");
977
- }
978
- if (resolved[0] === "operator") {
979
- if (resolved[1] === "enter") {
980
- if (resolved.length !== 2)
981
- throw usageError("Operator enter usage: yui operator enter.");
982
- const role = store.getGlobalRole("operator");
983
- if (role !== null)
984
- await prepareInteractiveCodexApp(role.activeAgentId, store, home);
985
- await ensureFileTaskController(home, { environment: process.env });
986
- await runtime.prepareGlobalRoleEnter("operator");
987
- tmux.attachRole("operator", "operator", "auto");
988
- return;
989
- }
990
- const result = runOperatorCommand(resolved.slice(1), store, { runtime, environment: process.env });
991
- if (result.kind === "output") {
992
- emit(result.output, false, result.data);
993
- return;
994
- }
995
- if (result.kind !== "session") {
996
- throw new Error("Operator command returned an invalid control result.");
997
- }
998
- await executeOperatorSessionControl(result, home, store, runtime, tmux, catalogs);
999
- return;
1000
- }
1001
- if (resolved[0] === "task") {
1002
- if (resolved[1] === "archive-preflight") {
1003
- const request = parseTaskArchiveArguments(resolved.slice(2), "archive-preflight");
1004
- if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== request.taskId) {
1005
- throw usageError("Archive inspection must remain within this Session's Task.");
1006
- }
1007
- const data = await inspectTaskArchive(workspaceCoordinator, request);
1008
- emit(renderTaskArchivePreflight(data), false, data);
1009
- return;
1010
- }
1011
- if (resolved[1] === "artifact") {
1012
- // File/directory artifacts live in the Task's local Git repository, so
1013
- // their save/read/list are asynchronous and handled here rather than in
1014
- // the synchronous runTaskCommand chain. Save commits exactly one path and
1015
- // returns a self-certifying commit-pinned reference; read pins to a commit
1016
- // for frozen evidence; list is an ordinary current read.
1017
- const action = resolved[2];
1018
- const taskId = resolved[3];
1019
- const usage = "Usage: yui task artifact list <task> | read <task> <relative-path> [<commit>] | "
1020
- + "save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]";
1021
- if (taskId === undefined || action === undefined || !["list", "read", "save"].includes(action)) {
1022
- throw usageError(usage);
1023
- }
1024
- if (process.env.YUI_SESSION_SCOPE === "task" && process.env.YUI_TASK_ID !== taskId) {
1025
- throw usageError("Artifact is outside the managed Task scope.");
1026
- }
1027
- if (store.getTask(taskId) === null)
1028
- throw usageError(`Task not found: ${taskId}.`);
1029
- let data;
1030
- if (action === "list") {
1031
- if (resolved.length !== 4)
1032
- throw usageError(usage);
1033
- data = await listArtifactsCapability(home, taskId);
1034
- }
1035
- else if (action === "read") {
1036
- const relativePath = resolved[4];
1037
- const commit = resolved[5];
1038
- if (relativePath === undefined || resolved.length > 6)
1039
- throw usageError(usage);
1040
- data = await readArtifactCapability(home, taskId, {
1041
- relativePath, ...(commit === undefined ? {} : { commit })
1042
- });
1043
- }
1044
- else {
1045
- // save: a delivery-authoritative action; a managed Task caller must be the current Leader.
1046
- taskLocalActor(store, process.env, taskId);
1047
- const relativePath = resolved[4];
1048
- const content = resolved[5];
1049
- if (relativePath === undefined || content === undefined)
1050
- throw usageError(usage);
1051
- const rest = resolved.slice(6);
1052
- let message;
1053
- let expectedHead;
1054
- for (let index = 0; index < rest.length; index += 1) {
1055
- const value = rest[index + 1];
1056
- if (rest[index] === "--message" && value !== undefined) {
1057
- message = value;
1058
- index += 1;
1059
- continue;
1060
- }
1061
- if (rest[index] === "--expected-head" && value !== undefined) {
1062
- expectedHead = value;
1063
- index += 1;
1064
- continue;
1065
- }
1066
- throw usageError(usage);
1067
- }
1068
- data = await saveArtifactCapability(home, taskId, {
1069
- relativePath, content,
1070
- ...(message === undefined ? {} : { message }),
1071
- ...(expectedHead === undefined ? {} : { expectedHead })
1072
- });
1073
- }
1074
- emit(JSON.stringify(data, null, 2), false, data);
1075
- return;
1076
- }
1077
- if (resolved[1] === "execution") {
1078
- if (resolved[2] === "stop") {
1079
- const request = parseTaskExecutionStopRequest(resolved.slice(3));
1080
- const result = stopTaskExecutionCommand(request, store, { environment: process.env });
1081
- try {
1082
- await ensureFileTaskController(home, { environment: process.env });
1083
- await runtime.stopTaskDurableJobs(result.taskId);
1084
- await runtime.stopTaskRoleSessions(result.taskId, result.roleNames);
1085
- await runtime.assertTaskPhysicalResourcesReleased(result.taskId);
1086
- finalizeStoppedTaskExecution(result.taskId, store);
1087
- }
1088
- catch (error) {
1089
- throw runtimeError("Task execution is stopped and durable progress is preserved, but physical runtime cleanup failed.", { cause: error });
1090
- }
1091
- emit(result.output, false, result);
1092
- return;
1093
- }
1094
- if (resolved[2] === "start") {
1095
- const taskId = parseTaskExecutionStartRequest(resolved.slice(3));
1096
- const task = store.getTask(taskId);
1097
- if (task === null)
1098
- throw usageError(`Task not found: ${taskId}.`);
1099
- // Reject managed Task callers before inspecting or starting runtime resources.
1100
- if (taskLocalActor(store, process.env, taskId) === "leader") {
1101
- throw usageError("Task execution stop/start requires the global Operator or a human user.");
1102
- }
1103
- if (task.executionGate.state === "stopped") {
1104
- await runtime.assertTaskPhysicalResourcesReleased(taskId);
1105
- }
1106
- await ensureFileTaskController(home, { environment: process.env });
1107
- const result = startTaskExecutionCommand(taskId, store, { environment: process.env });
1108
- // Idempotent start is also a reliable kick: if an earlier caller
1109
- // committed the gate but lost its Controller acknowledgement, retrying
1110
- // start re-signals the same durable wake without creating another one.
1111
- await runtime.notifyMailboxChanged({ kind: "role", taskId, roleName: "leader" });
1112
- emit(result.output, false, result);
1113
- return;
1114
- }
1115
- throw usageError("Task execution usage: yui task execution <stop|start> ...");
1116
- }
1117
- if (resolved[1] === "integration") {
1118
- const result = await runTaskIntegrationCommand(resolved.slice(2), store, home, {
1119
- environment: process.env,
1120
- jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
1121
- });
1122
- emit(result.output, false, result.data);
1123
- return;
1124
- }
1125
- if (resolved[1] === "change-set") {
1126
- const result = await runTaskChangeSetCommand(resolved.slice(2), store);
1127
- emit(result.output, false, result.data);
1128
- return;
1129
- }
1130
- if (resolved[1] === "publication" && (resolved[2] === "diff" || resolved[2] === "adopt")) {
1131
- const result = await runTaskPublicationAdoptCommand(resolved.slice(2), store, {
1132
- environment: process.env
1133
- });
1134
- emit(result.output, false, result.data);
1135
- return;
1136
- }
1137
- if (resolved[1] === "publication" && resolved[2] === "verify") {
1138
- const result = await runTaskPublicationVerifyCommand(resolved.slice(3), store, {
1139
- verifiers: {
1140
- github: createGitHubCliPublicationVerifier({
1141
- environmentPath: process.env.PATH
1142
- }),
1143
- gitlab: createGitLabCliPublicationVerifier({
1144
- environmentPath: process.env.PATH
1145
- })
1146
- },
1147
- environment: process.env
1148
- });
1149
- emit(result.output, false, result.data);
1150
- return;
1151
- }
1152
- if (resolved[1] === "overlap") {
1153
- const result = await runTaskOverlapCommand(resolved.slice(2), store);
1154
- emit(result.output, false, result.data);
1155
- return;
1156
- }
1157
- if (resolved[1] === "workflow"
1158
- && (resolved[2] === "run" || resolved[2] === "resume")) {
1159
- const result = await runWorkflowCommandAsync(resolved.slice(2), store, {
1160
- environment: process.env,
1161
- yuiHome: home,
1162
- ports: createReleaseWorkflowPorts({
1163
- home,
1164
- updatePorts: createUpdatePorts(process.env),
1165
- projectStore: store
1166
- })
1167
- });
1168
- if (result.kind !== "output") {
1169
- throw new Error(`Task workflow ${resolved[2]} returned an invalid control result.`);
1170
- }
1171
- emit(result.output, false, result.data);
1172
- return;
1173
- }
1174
- if (resolved[1] === "replace") {
1175
- const result = await runTaskWorkspaceCommand(resolved.slice(1), store, workspacePreparer);
1176
- emit(result.output, false, result.data);
1177
- return;
1178
- }
1179
- if (resolved[1] === "work" && resolved[2] === "isolate") {
1180
- const workItemId = resolved[3];
1181
- if (workItemId === undefined || resolved.length !== 4) {
1182
- throw usageError("Task work isolate usage: yui task work isolate <task>/<work>.");
1183
- }
1184
- const reference = cliWorkItemReference(workItemId, process.env);
1185
- const workspace = await workspaceCoordinator.isolateWorkItem(reference.taskId, reference.localId);
1186
- emit(`Created WorkItem workspace for ${reference.taskId}/${reference.localId}\nWorkspace: ${workspace.root}\n`, false, { workItemRef: reference, workspace });
1187
- return;
1188
- }
1189
- if (resolved[1] === "work" && resolved[2] === "review"
1190
- && resolved[3] === "cleanup") {
1191
- const reviewRoundId = resolved[4];
1192
- if (reviewRoundId === undefined || resolved.length !== 5) {
1193
- throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1194
- }
1195
- const reference = cliTaskRecordReference(reviewRoundId, "reviewRound", process.env);
1196
- const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1197
- if (removal === "dirty") {
1198
- throw usageError(`ReviewRound workspace is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1199
- }
1200
- emit(`Cleaned ReviewRound workspace ${reference.taskId}/${reference.localId} (${removal})\n`, false, { reviewRoundRef: reference, workspace: { removal } });
1201
- return;
1202
- }
1203
- if (resolved[1] === "work" && resolved[2] === "capture") {
1204
- const workItemId = resolved[3];
1205
- if (workItemId === undefined || resolved.length !== 4) {
1206
- throw usageError("Task work capture usage: yui task work capture <task>/<work>.");
1207
- }
1208
- const reference = cliWorkItemReference(workItemId, process.env);
1209
- assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1210
- const changeSets = await new WorkItemChangeSetManager(store).capture(reference.taskId, reference.localId, taskFinalReviewContract === undefined
1211
- ? {}
1212
- : { taskFinalReviewContract });
1213
- const qualified = `${reference.taskId}/${reference.localId}`;
1214
- emit(changeSets.length === 0
1215
- ? `WorkItem workspace has no changes to capture: ${qualified}\n`
1216
- : `Captured ChangeSets ${changeSets.map(({ id }) => id).join(", ")} from ${qualified}\n`, false, { workItemRef: reference, changeSets });
1217
- return;
1218
- }
1219
- if (resolved[1] === "work" && resolved[2] === "cleanup") {
1220
- const workItemId = resolved[3];
1221
- const disposition = resolved[4];
1222
- if (workItemId === undefined
1223
- || !["--runtime-only", "--integrated", "--abandon"].includes(disposition ?? "")
1224
- || resolved.length !== 5) {
1225
- throw usageError("Task work cleanup usage: yui task work cleanup <task>/<work> "
1226
- + "(--runtime-only|--integrated|--abandon).");
1227
- }
1228
- const reference = cliWorkItemReference(workItemId, process.env);
1229
- const qualified = `${reference.taskId}/${reference.localId}`;
1230
- assertTaskDeliveryAuthority(store, process.env, reference.taskId);
1231
- if (disposition === "--runtime-only") {
1232
- let runtimeCleanup;
1233
- try {
1234
- runtimeCleanup = await workspaceCoordinator.cleanupWorkItemRuntime(reference.taskId, reference.localId);
1235
- }
1236
- catch (error) {
1237
- throw cleanupCliError(error, `work-item:${qualified}`);
1238
- }
1239
- emit(`Released WorkItem runtime ${qualified}; retained its Session and worktree\n`, false, {
1240
- workItem: store.getWorkItem(reference.taskId, reference.localId),
1241
- runtime: { cleanup: runtimeCleanup },
1242
- worktree: { retained: true }
1243
- });
1244
- return;
1245
- }
1246
- const cleanedAs = disposition === "--integrated" ? "integrated" : "abandoned";
1247
- if (cleanedAs === "integrated") {
1248
- try {
1249
- await new WorkItemChangeSetManager(store).assertIntegrated(reference.taskId, reference.localId);
1250
- }
1251
- catch (error) {
1252
- throw cleanupCliError(error, `work-item:${qualified}`);
1253
- }
1254
- }
1255
- let removal;
1256
- try {
1257
- removal = await workspaceCoordinator.cleanupWorkItem(reference.taskId, reference.localId, cleanedAs);
1258
- }
1259
- catch (error) {
1260
- throw cleanupCliError(error, `work-item:${qualified}`);
1261
- }
1262
- if (removal === "dirty") {
1263
- throw usageError(`WorkItem worktree is dirty and was retained: ${qualified}.`, undefined, cleanupBlockedDetails("dirty-worktree", `work-item:${qualified}`, true));
1264
- }
1265
- emit(`Cleaned WorkItem worktree ${qualified} (${cleanedAs})\n`, false, {
1266
- workItem: store.getWorkItem(reference.taskId, reference.localId),
1267
- worktree: { removal, disposition: cleanedAs }
1268
- });
1269
- return;
1270
- }
1271
- if (resolved[1] === "work" && resolved[2] === "review"
1272
- && resolved[3] === "cleanup") {
1273
- const reviewRef = resolved[4];
1274
- if (reviewRef === undefined || resolved.length !== 5) {
1275
- throw usageError("Task work review cleanup usage: yui task work review cleanup <task>/<review-round>.");
1276
- }
1277
- const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1278
- const removal = await workspaceCoordinator.cleanupReviewRound(reference.taskId, reference.localId);
1279
- if (removal === "dirty") {
1280
- throw usageError(`ReviewRound worktree is dirty and was retained: ${reference.taskId}/${reference.localId}.`);
1281
- }
1282
- emit(`Cleaned ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1283
- reviewRound: store.getReviewRound(reference.taskId, reference.localId),
1284
- worktree: { removal }
1285
- });
1286
- return;
1287
- }
1288
- if (resolved[1] === "work" && resolved[2] === "review"
1289
- && resolved[3] === "preserve") {
1290
- const reviewRef = resolved[4];
1291
- if (reviewRef === undefined || resolved.length !== 5) {
1292
- throw usageError("Task work review preserve usage: yui task work review preserve <task>/<review-round>.");
1293
- }
1294
- const reference = cliTaskRecordReference(reviewRef, "reviewRound", process.env);
1295
- const round = preserveReviewRoundWorkspace(reference.taskId, reference.localId, store, { runtime, environment: process.env, yuiHome: home });
1296
- emit(`Preserved ReviewRound worktree ${reference.taskId}/${reference.localId}\n`, false, {
1297
- reviewRound: round
1298
- });
1299
- return;
1300
- }
1301
- let archiveRemoteDeliveryProof;
1302
- let archiveTaskReviewCandidate;
1303
- if (resolved[1] === "archive") {
1304
- const request = parseTaskArchiveArguments(resolved.slice(2));
1305
- if (request.sourceMessage !== undefined) {
1306
- if (request.force || request.disposition !== "integrated") {
1307
- throw usageError("Leader ordinary archive does not authorize --force or --abandon.");
1308
- }
1309
- const result = await callController(home, "capability.call", {
1310
- taskId: request.taskId, caller: resolveJobCaller(process.env, request.taskId),
1311
- request: { name: "task.archive", requestId: request.requestId,
1312
- input: { taskId: request.taskId, sourceMessage: request.sourceMessage, purpose: request.purpose } }
1313
- });
1314
- emit("Controller archive result; inspect task events if this Session ends before the receipt.\n", false, result);
1315
- return;
1316
- }
1317
- const { taskId, disposition, force } = validateTaskArchiveRequest(resolved.slice(2), store, {
1318
- runtime,
1319
- environment: process.env,
1320
- yuiHome: home
1321
- });
1322
- const task = store.getTask(taskId);
1323
- if (task === null)
1324
- throw new Error(`Task disappeared after archive validation: ${taskId}.`);
1325
- if (force || task.status === "archived") {
1326
- // Archive admission and mandatory audit commit before any fallible
1327
- // filesystem/provider work. Repeats report facts, never replay cleanup.
1328
- const admitted = runTaskCommand(resolved.slice(1), store, {
1329
- runtime, environment: process.env, yuiHome: home
1330
- });
1331
- if (force && admitted.kind === "output"
1332
- && admitted.data.changed) {
1333
- await workspaceCoordinator.cleanupArchivedTask(taskId, disposition);
1334
- }
1335
- const current = store.getTask(taskId);
1336
- const archive = taskArchiveDiagnostics(store, current);
1337
- emit(`Archived task ${taskId}\n${renderArchiveDiagnostics(archive)}`, false, { task: current, ...archive });
1338
- return;
1339
- }
1340
- {
1341
- if (disposition === "integrated") {
1342
- archiveTaskReviewCandidate = await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env);
1343
- archiveRemoteDeliveryProof = createTaskRemoteDeliveryProof(store, task, archiveTaskReviewCandidate ?? null);
1344
- assertTaskRemoteDeliveryIntegrated(archiveRemoteDeliveryProof.delivery);
1345
- }
1346
- const workItemIds = store.listManagedWorkspaces(task.id)
1347
- .flatMap(({ owner }) => owner.type === "work-item" ? [owner.workItemId] : []);
1348
- for (const workItemId of workItemIds) {
1349
- const item = store.getWorkItem(task.id, workItemId);
1350
- if (item?.status !== "accepted" || disposition !== "integrated")
1351
- continue;
1352
- try {
1353
- await new WorkItemChangeSetManager(store).assertIntegrated(task.id, item.id);
1354
- }
1355
- catch (error) {
1356
- throw cleanupCliError(error, `work-item:${task.id}/${item.id}`);
1357
- }
1358
- }
1359
- const cleanup = await workspaceCoordinator.cleanupTaskForArchive(task.id, disposition);
1360
- if (cleanup.status === "retained-dirty") {
1361
- 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));
1362
- }
1363
- if (cleanup.status === "failed") {
1364
- 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));
1365
- }
1366
- }
1367
- }
1368
- let taskRetirementProof;
1369
- if (resolved[1] === "retire") {
1370
- const taskId = resolved[2];
1371
- if (taskId !== undefined && !taskId.startsWith("--")) {
1372
- const task = store.getTask(taskId);
1373
- if (task?.status === "active" || task?.status === "draft") {
1374
- try {
1375
- taskRetirementProof = await new WorkItemChangeSetManager(store)
1376
- .assertRetirable(taskId);
1377
- }
1378
- catch (error) {
1379
- throw usageError(error instanceof Error ? error.message : String(error));
1380
- }
1381
- }
1382
- }
1383
- }
1384
- assertWorkItemExecutionDependenciesForCommand(resolved, store, process.env);
1385
- if (resolved[1] === "work" && resolved[2] === "dispatch") {
1386
- const workItemId = resolved[3];
1387
- const reference = workItemId === undefined
1388
- ? null
1389
- : cliWorkItemReference(workItemId, process.env);
1390
- const item = reference === null
1391
- ? null
1392
- : store.getWorkItem(reference.taskId, reference.localId);
1393
- const task = item === null ? null : store.getTask(item.taskId);
1394
- if (item !== null && task !== null) {
1395
- // Authority and pure Lane-shape checks precede every physical or
1396
- // durable workspace preparation performed for dispatch.
1397
- assertTaskDeliveryAuthority(store, process.env, task.id);
1398
- requireWorkItemAssignee(item);
1399
- workItemDispatchLanePlan(resolved, store, item);
1400
- }
1401
- // A rejected Candidate starts a new execution iteration. Release every
1402
- // terminal Lane Role runtime before preparing the new Lane workspaces;
1403
- // durable AgentRuns, Groups, Candidates, and workspace owners remain intact.
1404
- if (item?.status === "open"
1405
- && currentWorkItemExecutionGroup(item)?.lanes.every(({ disposition }) => disposition !== "open")) {
1406
- await workspaceCoordinator.cleanupWorkItemRuntime(item.taskId, item.id);
1407
- }
1408
- // Every Task needs an authoritative runtime owner before dispatch. A
1409
- // Gitless Task uses an empty Task-owned view; Project-backed WorkItems
1410
- // additionally receive their isolated Develop owner below.
1411
- if (item !== null && task !== null) {
1412
- await workspacePreparer.prepareTaskWorkspace(task.id);
1413
- }
1414
- // A Project-backed Worker WorkItem gets an isolated Develop owner before
1415
- // its Lane is prepared. A Leader-owned WorkItem intentionally executes
1416
- // in the Task main worktree and must not enter this isolation path.
1417
- if (item !== null
1418
- && task !== null
1419
- && task.projectBindings.length > 0
1420
- && item.assignee !== "leader"
1421
- && store.getWorkItemWorkspace(task.id, item.id) === null) {
1422
- await workspaceCoordinator.isolateWorkItem(item.taskId, item.id);
1423
- }
1424
- if (item !== null && task !== null) {
1425
- // For a new Group the preparer has already created deterministic
1426
- // worktrees, but the owner record is adopted by dispatch's aggregate
1427
- // transaction once its exact Lane ids exist.
1428
- }
1429
- }
1430
- let executionLaneWorkspaces;
1431
- // Held only for a new Group's dispatch: the per-Project maintenance fence
1432
- // spans Lane preparation and the adoption transaction, and projectPaths is
1433
- // the under-fence snapshot the adoption CAS revalidates.
1434
- let laneDispatchRelease;
1435
- let laneDispatchProjectPaths;
1436
- let workItemIntegrationProof;
1437
- if (resolved[1] === "work" && resolved[2] === "accept") {
1438
- const workItemId = resolved[3];
1439
- if (workItemId !== undefined && !workItemId.startsWith("--")) {
1440
- try {
1441
- const reference = cliWorkItemReference(workItemId, process.env);
1442
- workItemIntegrationProof = await new WorkItemChangeSetManager(store)
1443
- .assertIntegrated(reference.taskId, reference.localId, optionValue(resolved, "--candidate")) ?? undefined;
1444
- }
1445
- catch (error) {
1446
- throw usageError(error instanceof Error ? error.message : String(error));
1447
- }
1448
- }
1449
- }
1450
- let completionSummary;
1451
- let completionPublishedTreeProof;
1452
- let completionBaseFreshness;
1453
- let completionWarnings = [];
1454
- if (resolved[1] === "base" && resolved[2] === "status") {
1455
- const result = await runTaskBaseStatusCommand(resolved.slice(3), store);
1456
- emit(result.output, false, result.data);
1457
- return;
1458
- }
1459
- if (resolved[1] === "upstream") {
1460
- const result = await runTaskUpstreamCommand(resolved.slice(2), store, home, {
1461
- environment: process.env,
1462
- jobPort: createControllerIntegrationJobPort(home, { environment: process.env })
1463
- });
1464
- emit(result.output, false, result.data);
1465
- return;
1466
- }
1467
- if (resolved[1] === "complete" && resolved[2] !== undefined) {
1468
- const completionRequest = parseTaskCompletionRequest(resolved.slice(2));
1469
- completionSummary = completionRequest.summary;
1470
- const refreshRemote = completionRequest.refreshRemote;
1471
- const completion = preflightTaskCompletion(completionRequest.taskId, store, {
1472
- environment: process.env,
1473
- ...(taskFinalReviewContract === undefined
1474
- ? {}
1475
- : { taskFinalReviewContract })
1476
- }, completionRequest);
1477
- if (!completion.completed && !completion.activeTaskReview) {
1478
- // An explicit refresh must fetch the remote object graph before the
1479
- // Publication proof resolves its exact commit. Without the flag the
1480
- // command remains offline and preserves the existing proof-first path.
1481
- const refreshedFreshness = refreshRemote
1482
- ? await inspectTaskBaseFreshness(completionRequest.taskId, store, { refresh: true })
1483
- : undefined;
1484
- if (completionRequest.acceptedPublishedTreePublicationId !== undefined) {
1485
- completionPublishedTreeProof = await verifyTaskCompletionPublishedTree(completionRequest.taskId, completionRequest.acceptedPublishedTreePublicationId, store);
1486
- }
1487
- completionBaseFreshness = refreshedFreshness
1488
- ?? await inspectTaskBaseFreshness(completionRequest.taskId, store);
1489
- completionWarnings = assertTaskBaseFreshnessForCompletion(completionBaseFreshness, {
1490
- ...(completionPublishedTreeProof === undefined
1491
- ? {}
1492
- : {
1493
- acceptedPublishedTreeProjectId: completionPublishedTreeProof.projectId
1494
- })
1495
- });
1496
- for (const warning of completionWarnings) {
1497
- writeCliWarning(warning);
1498
- }
1499
- // Refresh observes remote objects only. Choosing and executing an
1500
- // upstream Integration belongs to its explicit command, never completion.
1501
- }
1502
- }
1503
- let releaseReviewHandoverLock;
1504
- if ((resolved[1] === "review"
1505
- && ["request", "retry"].includes(resolved[2] ?? ""))
1506
- || resolved[1] === "complete") {
1507
- const handoverLock = acquireHandoverLock(home);
1508
- releaseReviewHandoverLock = handoverLock.release;
1509
- }
1510
- try {
1511
- const preparedLanes = await prepareExecutionLaneWorkspacesForCommand(resolved, store, workspacePreparer, process.env);
1512
- if (preparedLanes !== undefined) {
1513
- executionLaneWorkspaces = preparedLanes.workspaces;
1514
- laneDispatchRelease = preparedLanes.release;
1515
- laneDispatchProjectPaths = preparedLanes.projectPaths;
1516
- }
1517
- const candidateSnapshots = await candidateSnapshotForTaskCommand(resolved, store, workspacePreparer, process.env, taskFinalReviewContract);
1518
- const actualTaskReviewCandidate = archiveRemoteDeliveryProof === undefined
1519
- ? await actualTaskReviewCandidateForTaskCommand(resolved, store, workspacePreparer, process.env)
1520
- : archiveTaskReviewCandidate;
1521
- const deltaRecheckPreflight = await deltaRecheckPreflightForTaskCommand(resolved.slice(1), store, actualTaskReviewCandidate);
1522
- // Read-only Host evidence for Session inspect and Role status/list. The command stays
1523
- // synchronous over persisted state; this is the live reading it prints
1524
- // beside those facts, prepared here because the Host is reached over a
1525
- // socket.
1526
- const liveHostObservations = await liveHostObservationsForTaskCommand(resolved, store, home);
1527
- const liveRunConfiguration = runConfigurationForHostObservation(liveHostObservations?.[resolved[5] ?? ""]);
1528
- // Physical preparation may precede the durable write, but Task status,
1529
- // workspace identity/cwd, and ManagedWorkspace ownership are adopted by
1530
- // one transaction. A failed attempt therefore leaves the Task Draft and
1531
- // owning no writable workspace.
1532
- let taskWorkspaceActivation;
1533
- if (resolved[1] === "activate" && resolved.length === 3) {
1534
- const taskId = resolved[2];
1535
- const task = taskId === undefined ? null : store.getTask(taskId);
1536
- if (task !== null && task.status === "draft") {
1537
- taskLocalActor(store, process.env, task.id);
1538
- taskWorkspaceActivation = await workspacePreparer.activateTaskWorkspace(task.id, process.env);
1539
- }
1540
- }
1541
- const result = runTaskCommand(resolved.slice(1), store, {
1542
- runtime,
1543
- environment: process.env,
1544
- yuiHome: home,
1545
- ...(taskFinalReviewContract === undefined
1546
- ? {}
1547
- : { taskFinalReviewContract }),
1548
- ...(completionSummary === undefined ? {} : { completionSummary }),
1549
- ...(completionPublishedTreeProof === undefined
1550
- ? {}
1551
- : { completionPublishedTreeProof }),
1552
- ...(workItemIntegrationProof === undefined ? {} : { workItemIntegrationProof }),
1553
- ...candidateSnapshots,
1554
- ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1555
- ...(taskWorkspaceActivation === undefined ? {} : { taskWorkspaceActivation }),
1556
- ...(laneDispatchProjectPaths === undefined ? {} : { laneDispatchProjectPaths }),
1557
- ...(actualTaskReviewCandidate === undefined
1558
- ? {}
1559
- : { actualTaskReviewCandidate }),
1560
- ...(archiveRemoteDeliveryProof === undefined
1561
- ? {}
1562
- : { archiveRemoteDeliveryProof }),
1563
- ...(deltaRecheckPreflight === undefined
1564
- ? {}
1565
- : { deltaRecheckPreflight }),
1566
- ...(liveRunConfiguration === undefined
1567
- ? {}
1568
- : { liveRunConfiguration }),
1569
- ...(liveHostObservations === undefined ? {} : { liveHostObservations }),
1570
- ...(taskRetirementProof === undefined ? {} : { taskRetirementProof }),
1571
- ...(validateAgentConfiguration === undefined
1572
- ? {}
1573
- : { validateAgentConfiguration })
1574
- });
1575
- // The dispatch transaction has now adopted (or rejected) the prepared
1576
- // Lane workspaces. Release the held fence so later output/review
1577
- // handling can take the per-Project fence itself.
1578
- if (laneDispatchRelease !== undefined) {
1579
- laneDispatchRelease();
1580
- laneDispatchRelease = undefined;
1581
- }
1582
- if (result.kind === "output") {
1583
- const requestedRound = reviewRoundFromCommandData(result.data);
1584
- const persistedRequestedRound = requestedRound === undefined
1585
- ? null
1586
- : store.getReviewRound(requestedRound.taskId, requestedRound.id);
1587
- let reviewOutput = "";
1588
- let reviewData;
1589
- const resumesReviewDispatch = (resolved[1] === "review" && resolved[2] === "request")
1590
- || (resolved[1] === "work" && resolved[2] === "review")
1591
- || (resolved[1] === "run" && resolved[2] === "retry");
1592
- const reviewDispatchNeeded = requestedRound?.status === "pending"
1593
- || (requestedRound?.status === "running"
1594
- && resumesReviewDispatch
1595
- && persistedRequestedRound?.executionGroup?.lanes.some((lane) => (lane.disposition === "open"
1596
- && (lane.currentRunId === undefined
1597
- || store.getRun(requestedRound.taskId, lane.currentRunId)?.status === "failed"))) === true);
1598
- if (reviewDispatchNeeded) {
1599
- try {
1600
- const workspace = requestedRound.status === "running"
1601
- ? store.getReviewRoundWorkspace(requestedRound.taskId, requestedRound.id)
1602
- : await workspacePreparer.prepareReviewRoundWorkspace(requestedRound.taskId, requestedRound.id);
1603
- if (workspace === null) {
1604
- throw new Error(`ReviewRound workspace is not ready: ${requestedRound.id}.`);
1605
- }
1606
- const reviewLaneWorkspaces = await prepareReviewLaneWorkspaces(requestedRound.taskId, requestedRound.id, store, workspacePreparer);
1607
- if (reviewLaneWorkspaces !== undefined) {
1608
- executionLaneWorkspaces = reviewLaneWorkspaces;
1609
- }
1610
- const storedRound = store.getReviewRound(requestedRound.taskId, requestedRound.id);
1611
- const freshTaskCandidate = storedRound?.scope === "task"
1612
- ? await snapshotActualTaskReviewCandidate(requestedRound.taskId, store, workspacePreparer)
1613
- : undefined;
1614
- const run = dispatchPreparedReviewRound(requestedRound.taskId, requestedRound.id, store, {
1615
- runtime,
1616
- environment: process.env,
1617
- yuiHome: home,
1618
- ...(taskFinalReviewContract === undefined
1619
- ? {}
1620
- : { taskFinalReviewContract }),
1621
- ...(freshTaskCandidate === undefined
1622
- ? {}
1623
- : { actualTaskReviewCandidate: freshTaskCandidate }),
1624
- ...(executionLaneWorkspaces === undefined ? {} : { executionLaneWorkspaces }),
1625
- ...(storedRound?.deltaRecheck === undefined
1626
- || deltaRecheckPreflight === undefined
1627
- ? {}
1628
- : {
1629
- deltaRecheckDiff: deltaRecheckPreflight.diffByProject
1630
- })
1631
- });
1632
- reviewOutput = run === null
1633
- ? `Review ${requestedRound.id} remains running\n`
1634
- : `Review queued as ${requestedRound.id} (${run.id})\n`;
1635
- reviewData = {
1636
- reviewRequest: run === null
1637
- ? {
1638
- kind: "running",
1639
- reviewerRoleName: requestedRound.reviewerRoleName,
1640
- activeReviewRoundId: requestedRound.id,
1641
- retryable: false
1642
- }
1643
- : {
1644
- kind: "started",
1645
- reviewerRoleName: requestedRound.reviewerRoleName,
1646
- reviewRoundId: requestedRound.id,
1647
- runId: run.id
1648
- },
1649
- reviewRound: store.getReviewRound(requestedRound.taskId, requestedRound.id),
1650
- ...(run === null ? {} : { reviewRun: run }),
1651
- workspace
1652
- };
1653
- }
1654
- catch (error) {
1655
- const message = error instanceof Error ? error.message : String(error);
1656
- await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1657
- const failed = failPendingReviewRound(requestedRound.taskId, requestedRound.id, message, store, {
1658
- runtime,
1659
- environment: process.env,
1660
- yuiHome: home
1661
- });
1662
- reviewOutput = `Review could not start: ${message}\n`
1663
- + "The failed ReviewRound was retained for Leader routing.\n";
1664
- reviewData = {
1665
- reviewRequest: {
1666
- kind: "unavailable",
1667
- reviewerRoleName: requestedRound.reviewerRoleName,
1668
- reviewRoundId: failed.id,
1669
- reason: message,
1670
- retryable: true
1671
- },
1672
- reviewRound: failed
1673
- };
1674
- }
1675
- }
1676
- if (resolved[1] === "project" && resolved[2] === "add") {
1677
- const taskId = resolved[3];
1678
- const task = taskId === undefined ? null : store.getTask(taskId);
1679
- if (task?.status === "active") {
1680
- await workspacePreparer.prepareTaskWorkspace(task.id);
1681
- }
1682
- }
1683
- const controlData = result.data;
1684
- const failureCode = controlData?.steer?.code ?? controlData?.interrupt?.code;
1685
- if (failureCode !== undefined) {
1686
- emitControlFailure(result.output, failureCode, result.data);
1687
- return;
1688
- }
1689
- const commandData = reviewData === undefined
1690
- ? result.data
1691
- : { command: result.data, ...reviewData };
1692
- const completionData = resolved[1] === "complete" ? {
1693
- // Review dispatch may have advanced or failed after the command
1694
- // prepared its Round. Report that observed stage, not preparation.
1695
- ...(reviewData === undefined ? {} : {
1696
- stage: reviewRoundFromCommandData(reviewData)?.status === "running"
1697
- ? "review-running"
1698
- : "review-blocked",
1699
- projectHeads: result.data.projectHeads
1700
- }),
1701
- baseFreshness: completionBaseFreshness ?? null,
1702
- warnings: completionWarnings
1703
- } : undefined;
1704
- emit(`${result.output}${reviewOutput}`, false, completionData === undefined
1705
- ? commandData
1706
- : { ...commandData, ...completionData });
1707
- return;
1708
- }
1709
- if (jsonOutput && result.kind !== "session-stop"
1710
- && result.kind !== "input-steer" && result.kind !== "input-interrupt") {
1711
- throw usageError("Task Role view/takeover requires an interactive terminal.");
1712
- }
1713
- if (result.kind === "session-stop") {
1714
- await ensureFileTaskController(home, { environment: process.env });
1715
- try {
1716
- await runtime.stopExactTaskRoleSession({
1717
- taskId: result.taskId,
1718
- roleName: result.roleName,
1719
- agentId: result.agentId,
1720
- adapterId: result.adapterId,
1721
- nativeSessionId: result.nativeSessionId,
1722
- sessionUpdatedAt: result.sessionUpdatedAt
1723
- });
1724
- }
1725
- catch (error) {
1726
- throw runtimeError(`Session stop was requested but physical Host cleanup did not complete: ${error instanceof Error ? error.message : String(error)}`);
1727
- }
1728
- emit(result.output, false, {
1729
- taskId: result.taskId,
1730
- roleName: result.roleName,
1731
- stopped: true,
1732
- reason: result.reason
1733
- });
1734
- return;
1735
- }
1736
- if (result.kind === "view") {
1737
- if (result.output !== undefined)
1738
- emit(result.output);
1739
- tmux.attachRole(result.taskId, result.roleName, "read-only");
1740
- return;
1741
- }
1742
- if (result.kind === "input-steer") {
1743
- // Core already persisted the Message and proved target + capability +
1744
- // writer fence. This is the single live edge: one native steer of the
1745
- // exact current Turn, with no retarget and no fallback to interrupt or
1746
- // queue. Its durable settlement flows through the steer receipt fold.
1747
- await ensureFileTaskController(home, { environment: process.env });
1748
- let control;
1749
- try {
1750
- control = await sendAgentHostSteerControl({
1751
- home,
1752
- scope: "task",
1753
- taskId: result.taskId,
1754
- roleName: result.roleName,
1755
- control: {
1756
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1757
- type: "steer-turn",
1758
- nativeSessionId: result.target.nativeSessionId,
1759
- nativeTurnId: result.target.nativeTurnId ?? result.target.attemptId,
1760
- authority: result.target.authority,
1761
- run: { attemptId: result.receiptId, boundedText: result.text }
1762
- }
1763
- });
1764
- }
1765
- catch (error) {
1766
- throw runtimeError(`Steer message ${result.messageId} is saved but the native steer did not complete: `
1767
- + `${error instanceof Error ? error.message : String(error)}. `
1768
- + "The Message is retained and its outcome is recorded from the Host; whether the "
1769
- + "Provider accepted it may be delivery-unknown. Re-read the Session before acting; "
1770
- + "do not reissue the same input under a new requestId or a different action.");
1771
- }
1772
- const steer = foldSteerLiveReceipt(control);
1773
- if (steer.state !== "steered") {
1774
- 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 });
1775
- return;
1776
- }
1777
- emit(steerReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, result.messageId, steer), false, {
1778
- taskId: result.taskId, roleName: result.roleName,
1779
- messageId: result.messageId, steer
1780
- });
1781
- return;
1782
- }
1783
- if (result.kind === "input-interrupt") {
1784
- // The single live edge for interrupt: one native cancel of the exact
1785
- // current Turn. Never a kill/restart/detach. Any then-handoff was
1786
- // already claimed durably by Core and is delivered once by the ordinary
1787
- // continuation path after this Turn reaches a proven terminal.
1788
- await ensureFileTaskController(home, { environment: process.env });
1789
- let control;
1790
- try {
1791
- control = await sendAgentHostCancelControl({
1792
- home,
1793
- scope: "task",
1794
- taskId: result.taskId,
1795
- roleName: result.roleName,
1796
- control: {
1797
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1798
- type: "cancel",
1799
- nativeOnly: true,
1800
- nativeSessionId: result.target.nativeSessionId,
1801
- // Native cancel names the exact original execution attempt it stops
1802
- // (Host matches request.attemptId === activeRunAttemptId). That is
1803
- // distinct from receiptId, the durable identity of this interrupt
1804
- // control operation — never send the operation id as the turn id.
1805
- attemptId: result.target.attemptId,
1806
- authority: result.target.authority
1807
- }
1808
- });
1809
- }
1810
- catch (error) {
1811
- recordTaskInterruptResult(store, result.taskId, result.receiptId, { state: "interrupt-unknown", outcome: "cancel-requested" });
1812
- throw runtimeError(`Interrupt ${result.receiptId} of ${result.taskId}/${result.roleName} did not complete: `
1813
- + `${error instanceof Error ? error.message : String(error)}. `
1814
- + "No process was killed; re-read the Session before retrying.");
1815
- }
1816
- const interrupt = foldInterruptLiveReceipt(control);
1817
- recordTaskInterruptResult(store, result.taskId, result.receiptId, interrupt);
1818
- if (interrupt.state !== "interrupt-requested") {
1819
- emitControlFailure(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), interrupt.state === "interrupt-unknown" ? "DELIVERY_UNKNOWN"
1820
- : interrupt.state === "interrupt-not-active" ? "NO_ACTIVE_TURN" : "INTERRUPT_REJECTED", { taskId: result.taskId, roleName: result.roleName, receiptId: result.receiptId, interrupt });
1821
- return;
1822
- }
1823
- emit(interruptReceiptOutput(result.output, `${result.taskId}/${result.roleName}`, interrupt), false, {
1824
- taskId: result.taskId, roleName: result.roleName,
1825
- ...(result.thenMessageId === undefined ? {} : { thenMessageId: result.thenMessageId }),
1826
- interrupt
1827
- });
1828
- return;
1829
- }
1830
- const syncAuthority = async (authorityResult) => {
1831
- let control;
1832
- try {
1833
- control = await sendAgentHostAuthorityControl({
1834
- home,
1835
- scope: "task",
1836
- taskId: authorityResult.taskId,
1837
- roleName: authorityResult.roleName,
1838
- control: {
1839
- protocol: AGENT_HOST_CONTROL_PROTOCOL,
1840
- type: "set-authority",
1841
- nativeSessionId: authorityResult.nativeSessionId,
1842
- authority: authorityResult.authority
1843
- }
1844
- });
1845
- }
1846
- catch (error) {
1847
- throw runtimeError(`Agent Host authority synchronization failed at epoch ${authorityResult.authority.epoch}: `
1848
- + `${error instanceof Error ? error.message : String(error)}. `
1849
- + `Durable authority is ${authorityResult.authority.owner}-owned; retry `
1850
- + `'yui task role release ${authorityResult.taskId} ${authorityResult.roleName}' `
1851
- + "to reconcile the Host.");
1852
- }
1853
- if (control.outcome !== "accepted"
1854
- || control.snapshot.nativeSessionId !== authorityResult.nativeSessionId
1855
- || control.snapshot.authorityEpoch !== authorityResult.authority.epoch
1856
- || control.snapshot.authorityOwner !== authorityResult.authority.owner
1857
- || control.snapshot.authorityHolderId !== authorityResult.authority.holderId) {
1858
- throw runtimeError(`Agent Host did not accept Provider authority epoch ${authorityResult.authority.epoch}: `
1859
- + (control.snapshot.detail ?? control.outcome)
1860
- + `. Durable authority is ${authorityResult.authority.owner}-owned; `
1861
- + "retry 'yui task role release "
1862
- + `${authorityResult.taskId} ${authorityResult.roleName}' to reconcile the Host.`);
1863
- }
1864
- return control;
1865
- };
1866
- await syncAuthority(result);
1867
- emit(result.output);
1868
- if (result.action === "release") {
1869
- runtime.notifyMailboxChanged({
1870
- kind: "role",
1871
- taskId: result.taskId,
1872
- roleName: result.roleName
1873
- });
1874
- return;
1875
- }
1876
- process.stdout.write("Provider input is now routed through the Agent Host PTY gateway. "
1877
- + "Use tmux detach (Ctrl-b d) to return authority to the Controller.\n");
1878
- try {
1879
- tmux.attachRole(result.taskId, result.roleName, "read-write");
1880
- }
1881
- finally {
1882
- const currentTask = store.getTask(result.taskId);
1883
- // Completing or retiring the Task from inside the takeover AgentRun owns
1884
- // Provider shutdown and clears the live binding. Do not turn that
1885
- // successful terminal transition into a failing best-effort release.
1886
- if (currentTask?.status === "active") {
1887
- const released = runTaskCommand(["role", "release", result.taskId, result.roleName], store, { runtime, environment: process.env, yuiHome: home });
1888
- if (released.kind !== "authority" || released.action !== "release") {
1889
- throw runtimeError("Provider authority release returned an invalid result.");
1890
- }
1891
- await syncAuthority(released);
1892
- emit(released.output);
1893
- runtime.notifyMailboxChanged({
1894
- kind: "role",
1895
- taskId: released.taskId,
1896
- roleName: released.roleName
1897
- });
1898
- }
1899
- }
1900
- return;
1901
- }
1902
- catch (error) {
1903
- await workspacePreparer.discardUnadoptedExecutionLaneWorkspaces(executionLaneWorkspaces);
1904
- if (laneDispatchRelease !== undefined) {
1905
- laneDispatchRelease();
1906
- laneDispatchRelease = undefined;
1907
- }
1908
- throw error;
1909
- }
1910
- finally {
1911
- if (releaseReviewHandoverLock !== undefined) {
1912
- releaseReviewHandoverLock();
1913
- releaseReviewHandoverLock = undefined;
1914
- }
1915
- }
1916
- }
1917
- if (resolved[0] === "jobs") {
1918
- emit(runJobCommand(resolved.slice(1), store, { runtime }));
1919
- return;
1920
- }
1921
- if (resolved[0] === "job") {
1922
- emit(await runDurableJobCommand(resolved.slice(1), {
1923
- home,
1924
- json: jsonOutput,
1925
- environment: process.env,
1926
- store
1927
- }));
1928
- return;
1929
- }
1930
- if (resolved[0] === "telemetry") {
1931
- emit(await runTelemetryCommand(resolved.slice(1), {
1932
- home,
1933
- json: jsonOutput,
1934
- environment: process.env,
1935
- store
1936
- }));
1937
- return;
1938
- }
1939
- throw usageError(`Command is not connected to the current TaskStore command routing: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
1940
- }
1941
- finally {
1942
- await telemetry?.sink.close();
1943
- }
1944
- }
1945
- function explicitReleaseActivationDriver() {
1946
- if (args.length !== 3
1947
- || args[0] !== "release"
1948
- || args[1] !== "activate"
1949
- || args[2].startsWith("-")) {
1950
- return null;
1951
- }
1952
- return resolveReleaseActivationDriver(resolveYuiHome(process.env), args[2], fileURLToPath(import.meta.url));
1953
- }
1954
- async function preflightManagedTaskControlPlane() {
1955
- if (taskFinalReviewInvocation.error !== undefined) {
1956
- throw new Error(taskFinalReviewInvocation.error);
1957
- }
1958
- if (process.env.YUI_SESSION_SCOPE !== "task") {
1959
- if (process.env.YUI_SESSION_SCOPE === "global") {
1960
- return await preflightManagedGlobalControlPlane();
1961
- }
1962
- if (taskFinalReviewInvocation.request !== undefined) {
1963
- throw new Error("Task final-review contract may only be established from the Task Leader's managed Session.");
1964
- }
1965
- return { contract: undefined, verifiedStore: undefined };
1966
- }
1967
- const internalCallback = args[0] === "internal"
1968
- && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
1969
- const home = resolveYuiHome(process.env);
1970
- assertManagedSessionManifest(home, "task");
1971
- const diagnosticTarget = taskDiagnosticTarget(args);
1972
- const diagnostic = diagnosticTarget !== undefined
1973
- && diagnosticTarget === process.env.YUI_TASK_ID;
1974
- if (diagnosticTarget !== undefined && !diagnostic) {
1975
- throw usageError("Diagnostics must remain within this Session's Task.");
1976
- }
1977
- // One gate for every managed command: the current CLI, Home, and Controller
1978
- // must agree. Internal callbacks must still be able to append their immutable
1979
- // fact while the Controller is offline.
1980
- await assertRuntimeCoherence({ actualHome: home }, { checkController: !internalCallback && !diagnostic });
1981
- const verifiedStore = openCurrentTaskStore(home);
1982
- // One authority for "may this process act as this Task Role?". Yui's own
1983
- // internal callbacks run inside the Host process Yui itself launched; an Agent
1984
- // command proves it is the Role's current runtime with its per-Session caller
1985
- // key. Nothing here
1986
- // gates on the current AgentRun: which AgentRun is active is durable state that the
1987
- // command needing it reads, never a fact frozen into a process environment.
1988
- const runtime = internalCallback
1989
- ? undefined
1990
- : diagnostic
1991
- ? resolveManagedTaskReader(verifiedStore, process.env)
1992
- : requireManagedTaskCaller(verifiedStore, process.env);
1993
- if (diagnostic && runtime?.roleName !== "leader"
1994
- && !(args[1] === "show"
1995
- || (args[1] === "role" && args[2] === "session" && args[3] === "inspect"
1996
- && args[5] === runtime?.roleName))) {
1997
- // Non-Leaders keep their Assignment-scoped read authorization.
1998
- requireManagedTaskCaller(verifiedStore, process.env);
1999
- }
2000
- const request = taskFinalReviewInvocation.request;
2001
- if (request !== undefined && diagnostic)
2002
- requireManagedTaskCaller(verifiedStore, process.env);
2003
- if (request === undefined) {
2004
- return { contract: undefined, verifiedStore };
2005
- }
2006
- if (runtime === undefined || runtime.roleName !== "leader") {
2007
- throw new Error("Only the Task Leader's managed Session may establish a final-review contract.");
2008
- }
2009
- if (request.taskId !== runtime.taskId) {
2010
- throw new Error(`Task final-review contract Task id mismatch: expected ${runtime.taskId}, found ${request.taskId}.`);
2011
- }
2012
- const recordedContract = resolveRecordedTaskFinalReviewContract(runtime.taskId, verifiedStore.listWorkItems(runtime.taskId), verifiedStore.listReviewRounds(runtime.taskId))?.effective;
2013
- if (recordedContract !== undefined
2014
- && recordedContract.reviewerRoleName !== request.reviewerRoleName) {
2015
- throw new Error(`Task final-review Reviewer mismatch: expected ${recordedContract.reviewerRoleName}, `
2016
- + `found ${request.reviewerRoleName}.`);
2017
- }
2018
- return {
2019
- contract: createTaskFinalReviewContract({
2020
- taskId: runtime.taskId,
2021
- reviewerRoleName: request.reviewerRoleName
2022
- }),
2023
- verifiedStore
2024
- };
2025
- }
2026
- async function preflightManagedGlobalControlPlane() {
2027
- if (taskFinalReviewInvocation.request !== undefined) {
2028
- throw new Error("Task final-review contract may only be established from the Task Leader's managed Session.");
2029
- }
2030
- const home = resolveYuiHome(process.env);
2031
- const manifest = assertManagedSessionManifest(home, "global");
2032
- const expectedRoleKind = process.env.YUI_ROLE === "operator" ? "operator" : "global";
2033
- if (manifest.owner.scope !== "global"
2034
- || manifest.roleKind !== expectedRoleKind) {
2035
- throw new ManagedRuntimeDriftError("Managed global invocation does not match its Session Manifest.");
2036
- }
2037
- if (expectedRoleKind === "operator" && ["doctor", "upgrade", "update"].includes(args[0] ?? "")) {
2038
- // These commands inspect/adopt the storage contract themselves. Requiring
2039
- // current storage before reaching upgrade would make recovery impossible.
2040
- return { contract: undefined, verifiedStore: undefined };
2041
- }
2042
- await assertRuntimeCoherence({ actualHome: home }, {
2043
- checkController: !(expectedRoleKind === "operator" && operatorOfflineCommand(args))
2044
- });
2045
- const verifiedStore = openCurrentTaskStore(home);
2046
- const internalCallback = args[0] === "internal"
2047
- && ["agent-host", "session-notify", "runtime-hook"].includes(args[1] ?? "");
2048
- if (!internalCallback && !operatorOfflineCommand(args)) {
2
+ // No static control-plane imports: the product never resolves/initializes Yui Home.
3
+ const arguments_ = process.argv.slice(2);
4
+ let commandOffset = 0;
5
+ while (arguments_[commandOffset] === '--json')
6
+ commandOffset++;
7
+ const commandArguments = arguments_.slice(commandOffset);
8
+ if (commandArguments[0] === 'agent' || (commandArguments[0] === 'help' && commandArguments[1] === 'agent')) {
9
+ let outputFailed = false;
10
+ // Cover early help/config writes as well as execution UI disconnection.
11
+ process.stdout.on('error', () => { outputFailed = true; process.exitCode = 1; });
12
+ process.stderr.on('error', () => { process.exitCode = 1; });
13
+ let failure = { code: 'agent_execution', field: 'startup', nextAction: 'Check the local installation; no execution was replayed.' };
14
+ try {
15
+ const { runProductCommand, productFailure } = await import('./nativeAgent/product/index.js');
2049
16
  try {
2050
- requireManagedGlobalCaller(verifiedStore, process.env);
17
+ const productArguments = commandArguments[0] === 'help' ? ['--help'] : commandArguments.slice(1);
18
+ const code = await runProductCommand([...productArguments, ...(commandOffset ? ['--json'] : [])]);
19
+ process.exitCode = outputFailed ? 1 : code;
2051
20
  }
2052
21
  catch (error) {
2053
- verifiedStore.close();
22
+ failure = productFailure(error);
2054
23
  throw error;
2055
24
  }
2056
25
  }
2057
- return { contract: undefined, verifiedStore };
2058
- }
2059
- function assertManagedSessionManifest(home, scope) {
2060
- const manifestPath = process.env.YUI_SESSION_MANIFEST;
2061
- if (manifestPath === undefined) {
2062
- throw new ManagedRuntimeDriftError("Managed control-plane invocation requires its Session Manifest.");
2063
- }
2064
- let manifest;
2065
- try {
2066
- manifest = readSessionBootstrapManifest(manifestPath);
2067
- }
2068
- catch (error) {
2069
- throw new ManagedRuntimeDriftError("Managed Session Manifest could not be verified.", { cause: error });
2070
- }
2071
- if (manifest.owner.scope !== scope) {
2072
- throw new ManagedRuntimeDriftError("Managed invocation scope does not match its Session Manifest.");
2073
- }
2074
- if (scope === "task" && (manifest.owner.scope !== "task"
2075
- || manifest.owner.taskId !== process.env.YUI_TASK_ID)) {
2076
- throw new ManagedRuntimeDriftError("Managed Task invocation does not match its Session Manifest owner.");
2077
- }
2078
- const expectedPath = resolve(home, "runtime", "session-manifests", `${manifest.digest}.json`);
2079
- // YUI_HOME is canonicalized (its macOS /var -> /private/var prefix is a
2080
- // system symlink), so compare the manifest in the same canonical space.
2081
- let canonicalManifestPath;
2082
- try {
2083
- canonicalManifestPath = realpathSync(resolve(manifestPath));
2084
- }
2085
- catch {
2086
- canonicalManifestPath = resolve(manifestPath);
2087
- }
2088
- if (canonicalManifestPath !== expectedPath) {
2089
- throw new ManagedRuntimeDriftError("Managed Session Manifest path is outside this YUI_HOME.");
2090
- }
2091
- return manifest;
2092
- }
2093
- function cleanupCliError(error, fallbackResource) {
2094
- if (error instanceof CleanupInspectionError) {
2095
- return usageError(error.message, undefined, cleanupBlockedDetails(error.checks[0]?.reason ?? "cleanup-failed", fallbackResource, true, error.checks));
2096
- }
2097
- if (error instanceof WorkspaceCleanupBlockedError) {
2098
- return usageError(error.message, undefined, cleanupBlockedDetails(error.reason, error.resource, error.retryable));
2099
- }
2100
- return new CliError("RUNTIME_ERROR", error instanceof Error ? error.message : String(error), undefined, cleanupBlockedDetails("cleanup-failed", fallbackResource, true), { cause: error });
2101
- }
2102
- function cleanupBlockedDetails(reason, resource, retryable, checks) {
2103
- return {
2104
- status: "blocked",
2105
- blockedBy: [{ resource, reason, retryable }],
2106
- remainingResources: [resource],
2107
- retryable,
2108
- ...(checks === undefined ? {} : { checks })
2109
- };
2110
- }
2111
- function cliWorkItemReference(value, environment) {
2112
- try {
2113
- return resolveTaskRecordReference(value, {
2114
- kind: "workItem",
2115
- label: "Work Item reference",
2116
- ...(environment.YUI_TASK_ID === undefined
2117
- ? {}
2118
- : { contextTaskId: environment.YUI_TASK_ID })
2119
- });
2120
- }
2121
- catch (error) {
2122
- throw usageError(error instanceof Error ? error.message : String(error));
2123
- }
2124
- }
2125
- function cliTaskRecordReference(value, kind, environment) {
2126
- try {
2127
- return resolveTaskRecordReference(value, {
2128
- kind,
2129
- label: kind === "run" ? "AgentRun reference" : "ReviewRound reference",
2130
- ...(environment.YUI_TASK_ID === undefined
2131
- ? {}
2132
- : { contextTaskId: environment.YUI_TASK_ID })
2133
- });
2134
- }
2135
- catch (error) {
2136
- throw usageError(error instanceof Error ? error.message : String(error));
2137
- }
2138
- }
2139
- function assertWorkItemExecutionDependenciesForCommand(args, store, environment) {
2140
- let item;
2141
- if (args[0] === "task" && args[1] === "work" && args[2] === "dispatch"
2142
- && args[3] !== undefined) {
2143
- const reference = cliWorkItemReference(args[3], environment);
2144
- item = store.getWorkItem(reference.taskId, reference.localId) ?? undefined;
2145
- }
2146
- else if (args[0] === "task" && args[1] === "run" && args[2] === "retry"
2147
- && args[3] !== undefined) {
2148
- const reference = cliTaskRecordReference(args[3], "run", environment);
2149
- const run = store.getRun(reference.taskId, reference.localId);
2150
- item = run?.purpose === "execution" && run.workItemId !== undefined
2151
- ? store.getWorkItem(run.taskId, run.workItemId) ?? undefined
2152
- : undefined;
2153
- }
2154
- if (item === undefined)
2155
- return;
2156
- assertWorkItemDependenciesCompletedForCommand(store, item);
2157
- }
2158
- async function candidateSnapshotForTaskCommand(args, store, preparer, environment, taskFinalReviewContract) {
2159
- if (args[0] !== "task" || args[1] !== "work" || args[2] !== "update"
2160
- || args[3] === undefined || args[4] !== "done")
2161
- return {};
2162
- const reference = cliWorkItemReference(args[3], environment);
2163
- return snapshotWorkItemCandidate(store, preparer, reference.taskId, reference.localId, taskFinalReviewContract);
2164
- }
2165
- async function prepareExecutionLaneWorkspacesForCommand(args, store, preparer, environment) {
2166
- const isDispatch = args[0] === "task"
2167
- && args[1] === "work"
2168
- && args[2] === "dispatch"
2169
- && args[3] !== undefined;
2170
- if (!isDispatch)
2171
- return undefined;
2172
- const itemRef = cliWorkItemReference(args[3], environment);
2173
- const item = store.getWorkItem(itemRef.taskId, itemRef.localId);
2174
- if (item === null || item.assignee === undefined)
2175
- return undefined;
2176
- const plan = workItemDispatchLanePlan(args, store, item);
2177
- if (plan.roles.length === 0)
2178
- return undefined;
2179
- for (const roleName of plan.roles) {
2180
- if (store.getRole(item.taskId, roleName) === null) {
2181
- throw usageError(`Task Role not found: ${item.taskId}/${roleName}.`);
2182
- }
2183
- if (store.getActiveRun(item.taskId, roleName) !== null) {
2184
- throw usageError(`${item.taskId}/${roleName} already has an active turn.`);
2185
- }
2186
- }
2187
- const held = await preparer.acquireTaskProjectMaintenanceLocks(item.taskId);
2188
- const map = new Map();
2189
- try {
2190
- assertTaskDeliveryAuthority(store, environment, item.taskId);
2191
- const currentItem = store.getWorkItem(item.taskId, item.id);
2192
- if (currentItem?.revision !== item.revision
2193
- || JSON.stringify(workItemDispatchLanePlan(args, store, currentItem)) !== JSON.stringify(plan)) {
2194
- throw new Error(`Work item dispatch changed while waiting for Project maintenance: ${item.id}.`);
2195
- }
2196
- if (held.current.status !== "active" || held.current.executionGate.state !== "enabled"
2197
- || plan.roles.some(roleName => store.getRole(item.taskId, roleName) === null
2198
- || store.getActiveRun(item.taskId, roleName) !== null)) {
2199
- throw new Error(`Task/Role dispatch state changed while waiting for Project maintenance: ${item.taskId}.`);
2200
- }
2201
- const projectPaths = new Map();
2202
- for (const { projectId } of held.current.projectBindings) {
2203
- const project = store.getProject(projectId);
2204
- if (project === null)
2205
- throw new Error(`Project not found: ${projectId}.`);
2206
- projectPaths.set(projectId, project.path);
2207
- }
2208
- const source = item.assignee === "leader"
2209
- ? store.getTaskWorkspace(item.taskId)
2210
- : store.getWorkItemWorkspace(item.taskId, item.id);
2211
- if (source === null) {
2212
- throw usageError(`Execution Lane source workspace is not ready: ${item.id}.`);
2213
- }
2214
- const inputHeads = await preparer.snapshotExecutionLaneInputHeads(source, item.writeProjectIds);
2215
- for (const laneId of plan.laneIds) {
2216
- map.set(laneId, await preparer.prepareExecutionLaneWorkspace(item.taskId, plan.groupId, laneId, { purpose: "execution", workItemId: item.id, inputHeads }, { current: held.current }));
2217
- }
2218
- return { workspaces: map, release: held.release, projectPaths };
2219
- }
2220
- catch (error) {
2221
- await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
2222
- held.release();
2223
- throw error;
2224
- }
2225
- }
2226
- function workItemDispatchLanePlan(args, store, item) {
2227
- if (item.assignee === undefined) {
2228
- throw usageError(`Work Item has no Task Role assignee: ${item.id}.`);
2229
- }
2230
- const roles = args.flatMap((value, index) => (value === "--lane-role" && args[index + 1] !== undefined
2231
- ? [args[index + 1]]
2232
- : []));
2233
- const groupId = `execution-group-${store.peekNextRunId(item.taskId)}`;
2234
- return {
2235
- groupId,
2236
- ...planReplicatedWorkItemLanes(item.assignee, roles, groupId)
2237
- };
2238
- }
2239
- async function prepareReviewLaneWorkspaces(taskId, reviewRoundId, store, preparer) {
2240
- const round = store.getReviewRound(taskId, reviewRoundId);
2241
- const group = round?.executionGroup;
2242
- if (round === null || round === undefined || group === undefined)
2243
- return undefined;
2244
- const map = new Map();
2245
- try {
2246
- for (const lane of group.lanes.filter(({ disposition }) => disposition === "open")) {
2247
- map.set(lane.id, await preparer.prepareExecutionLaneWorkspace(taskId, group.id, lane.id, {
2248
- purpose: "review",
2249
- reviewRoundId
2250
- }));
2251
- }
2252
- }
2253
- catch (error) {
2254
- await preparer.discardUnadoptedExecutionLaneWorkspaces(map);
2255
- throw error;
2256
- }
2257
- return map;
2258
- }
2259
- async function actualTaskReviewCandidateForTaskCommand(args, store, preparer, environment) {
2260
- if (args[0] !== "task")
2261
- return undefined;
2262
- let taskId;
2263
- let decisionSupportRead = false;
2264
- if (args[1] === "complete" && args[2] !== undefined) {
2265
- const task = store.getTask(args[2]);
2266
- if (task === null || task.status !== "active" || task.projectBindings.length === 0) {
2267
- return undefined;
2268
- }
2269
- // Every Project-backed completion must freeze
2270
- // a clean committed Task-main snapshot. Review policy only decides whether
2271
- // that head also needs an independent ReviewRound.
2272
- taskId = task.id;
2273
- }
2274
- else if (args[1] === "review"
2275
- && args[2] === "request"
2276
- && args[3] !== undefined) {
2277
- taskId = store.getTask(args[3])?.id;
2278
- }
2279
- else if (args[1] === "review"
2280
- && args[2] === "retry"
2281
- && args[3] !== undefined) {
2282
- const reference = cliTaskRecordReference(args[3], "reviewRound", environment);
2283
- const round = store.getReviewRound(reference.taskId, reference.localId);
2284
- if (round !== null && round.scope === "task") {
2285
- taskId = reference.taskId;
2286
- }
2287
- }
2288
- else if (args[1] === "work"
2289
- && args[2] === "review"
2290
- && args[3] === "retry"
2291
- && args[4] !== undefined) {
2292
- const reference = cliTaskRecordReference(args[4], "reviewRound", environment);
2293
- const round = store.getReviewRound(reference.taskId, reference.localId);
2294
- if (round !== null && round.scope === "task") {
2295
- taskId = reference.taskId;
2296
- }
2297
- }
2298
- else if (args[1] === "run"
2299
- && (args[2] === "retry" || args[2] === "settle")
2300
- && args[3] !== undefined) {
2301
- const reference = cliTaskRecordReference(args[3], "run", environment);
2302
- const run = store.getRun(reference.taskId, reference.localId);
2303
- const round = run?.reviewRoundId === undefined
2304
- ? null
2305
- : store.getReviewRound(reference.taskId, run.reviewRoundId);
2306
- if (run?.purpose === "review"
2307
- && round !== null
2308
- && round.scope === "task") {
2309
- taskId = reference.taskId;
2310
- }
2311
- }
2312
- else if (args[1] === "archive"
2313
- && args[2] !== undefined
2314
- && args.includes("--integrated")) {
2315
- taskId = store.getTask(args[2])?.id;
2316
- }
2317
- else if ((args[1] === "show"
2318
- || args[1] === "next-action"
2319
- || args[1] === "remote-delivery")
2320
- && args[2] !== undefined) {
2321
- taskId = store.getTask(args[2])?.id;
2322
- decisionSupportRead = true;
2323
- }
2324
- const status = taskId === undefined ? undefined : store.getTask(taskId)?.status;
2325
- if (taskId === undefined || (status !== "active" && status !== "cancelled"))
2326
- return undefined;
2327
- try {
2328
- return await snapshotActualTaskReviewCandidate(taskId, store, preparer, !decisionSupportRead);
2329
- }
2330
- catch (error) {
2331
- if (decisionSupportRead && error instanceof CliError)
2332
- return undefined;
2333
- throw error;
2334
- }
2335
- }
2336
- async function snapshotActualTaskReviewCandidate(taskId, store, preparer, prepareWorkspace = true) {
2337
- const task = store.getTask(taskId);
2338
- if (task === null)
2339
- throw usageError(`Task not found: ${taskId}.`);
2340
- if (task.projectBindings.length === 0) {
2341
- throw usageError(`Final Task Review requires a Project-backed Task: ${task.id}.`);
2342
- }
2343
- // Mutation preflights reconcile before freezing a candidate. A decision-
2344
- // support read only observes existing heads; it must not migrate Sessions,
2345
- // create worktrees or acquire workspace ownership as a side effect.
2346
- if (prepareWorkspace)
2347
- await preparer.prepareTaskWorkspace(task.id);
2348
- const workspace = store.getTaskWorkspace(task.id);
2349
- if (workspace === null
2350
- || workspace.owner.type !== "task"
2351
- || workspace.owner.taskId !== task.id) {
2352
- throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
2353
- }
2354
- try {
2355
- const snapshot = await preparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(({ projectId }) => projectId));
2356
- const heads = new Map(snapshot.projects.map(({ projectId, headCommit }) => ([projectId, headCommit])));
2357
- return {
2358
- schemaVersion: 1,
2359
- projects: task.projectBindings.map(({ projectId }) => {
2360
- const commit = heads.get(projectId);
2361
- if (commit === undefined) {
2362
- throw new Error(`Task main snapshot omitted Project ${projectId}.`);
2363
- }
2364
- return { projectId, commit };
2365
- })
2366
- };
2367
- }
2368
- catch (error) {
2369
- throw usageError(`Actual Task Project head verification failed for ${task.id}: `
2370
- + `${error instanceof Error ? error.message : String(error)}`);
2371
- }
2372
- }
2373
- /**
2374
- * Issue 07: computes the delta-recheck assessment for `task review request
2375
- * --delta-recheck`. Runs only for that exact command; every other command
2376
- * returns undefined. Technical evidence boundaries fail closed here; semantic
2377
- * risk remains a Leader and Project-policy decision.
2378
- */
2379
- async function deltaRecheckPreflightForTaskCommand(args, store, actualTaskReviewCandidate) {
2380
- if (args[0] !== "task" || args[1] !== "review" || args[2] !== "request") {
2381
- return undefined;
2382
- }
2383
- if (!args.includes("--delta-recheck"))
2384
- return undefined;
2385
- const taskId = args[3];
2386
- if (taskId === undefined)
2387
- return undefined;
2388
- const task = store.getTask(taskId);
2389
- if (task === null || task.status !== "active")
2390
- return undefined;
2391
- if (actualTaskReviewCandidate === undefined)
2392
- return undefined;
2393
- // The previous Round is the latest completed Task-final Round that accepted
2394
- // a head (a full Review or an equivalent-and-accepted delta). A
2395
- // non-accepted delta cannot be the base for a new delta.
2396
- const previous = [...store.listReviewRounds(task.id)]
2397
- .filter((round) => isCompletedTaskReviewEvidence(store, round))
2398
- .sort((left, right) => left.id.localeCompare(right.id, undefined, { numeric: true }))
2399
- .at(-1);
2400
- if (previous === undefined) {
2401
- throw usageError("Delta-recheck requires a previous completed Task-final Review that accepted a head.");
2402
- }
2403
- const repositoryPaths = {};
2404
- const taskWorkspace = store.getTaskWorkspace(task.id);
2405
- if (taskWorkspace === null || taskWorkspace.owner.type !== "task") {
2406
- throw usageError(`Task has no authoritative main workspace: ${task.id}.`);
2407
- }
2408
- for (const candidateProject of actualTaskReviewCandidate.projects) {
2409
- const entry = workspaceProjectEntry(taskWorkspace, candidateProject.projectId);
2410
- if (entry === undefined) {
2411
- throw usageError(`Delta-recheck Task workspace Project not found: ${candidateProject.projectId}.`);
2412
- }
2413
- repositoryPaths[candidateProject.projectId] = entry.path;
2414
- }
2415
- const assessment = await assessDeltaRecheck({
2416
- repositoryPaths,
2417
- previousRound: previous,
2418
- candidate: actualTaskReviewCandidate,
2419
- git: new NodeGitWorkspace()
2420
- });
2421
- if (assessment.kind === "ineligible") {
2422
- throw usageError(`Delta-recheck is technically unavailable: ${assessment.reason}`);
2423
- }
2424
- return assessment.preflight;
2425
- }
2426
- function reviewRoundFromCommandData(data) {
2427
- if (typeof data !== "object" || data === null || !("reviewRound" in data))
2428
- return undefined;
2429
- const round = data.reviewRound;
2430
- if (typeof round !== "object" || round === null)
2431
- return undefined;
2432
- const value = round;
2433
- return typeof value.id === "string"
2434
- && typeof value.taskId === "string"
2435
- && typeof value.reviewerRoleName === "string"
2436
- && typeof value.status === "string"
2437
- ? {
2438
- id: value.id,
2439
- taskId: value.taskId,
2440
- reviewerRoleName: value.reviewerRoleName,
2441
- status: value.status
2442
- }
2443
- : undefined;
2444
- }
2445
- async function executeOperatorSessionControl(control, home, store, runtime, tmux, catalogs) {
2446
- if (jsonOutput)
2447
- throw usageError("Operator new and resume do not support --json.");
2448
- if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
2449
- throw usageError("Operator new and resume require an interactive terminal.");
2450
- }
2451
- const role = store.getGlobalRole("operator");
2452
- if (role === null)
2453
- throw usageError("Operator is not configured. Run yui setup first.");
2454
- const sessionSet = store.getGlobalRoleSessionSet(role.name);
2455
- const active = sessionSet?.sessions[sessionSet.activeAgentId];
2456
- const paneRunning = tmux.probeRoleStatus("operator", "operator") === "running";
2457
- if (paneRunning && active === undefined) {
2458
- throw usageError("Operator is running but its native session has not been recorded yet. "
2459
- + "Record the exact native session before switching sessions.");
2460
- }
2461
- if (control.action === "resume"
2462
- && paneRunning
2463
- && control.targetAgentId === active?.agentId
2464
- && active !== undefined
2465
- && operatorSessionRef(active) === control.ref) {
2466
- await prepareInteractiveCodexApp(control.targetAgentId, store, home);
2467
- tmux.attachRole("operator", "operator", "auto");
2468
- return;
2469
- }
2470
- const handle = terminalIo();
2471
- try {
2472
- if (control.targetAgentId !== role.activeAgentId) {
2473
- const binding = role.agentBindings[control.targetAgentId];
2474
- if (binding === undefined) {
2475
- throw usageError(`Operator Agent is not bound: ${control.targetAgentId}.`);
2476
- }
2477
- handle.io.write([
2478
- `Switching to ${adapterLabel(binding.adapterId)} (${binding.agentId})`,
2479
- "",
2480
- "Saved configuration",
2481
- ` Model ${binding.config.model ?? "CLI default"}`,
2482
- ` Effort ${binding.config.effort ?? "CLI default"}`,
2483
- ""
2484
- ].join("\n"));
2485
- const update = (await handle.io.question("Update this configuration? [y/N]: "))?.trim().toLowerCase();
2486
- if (update === "y" || update === "yes") {
2487
- const resolution = await resolveGlobalRoleAgentConfigurationArguments(role.name, binding.agentId, selectionPorts(store, catalogs), handle.io);
2488
- if (resolution.kind !== "resolved") {
2489
- process.stdout.write("Cancelled.\n");
2490
- return;
2491
- }
2492
- const updated = runGlobalRoleCommand(resolution.args.slice(2), store, { yuiHome: home, env: process.env });
2493
- if (typeof updated !== "string") {
2494
- throw new Error("Operator Agent configuration returned an invalid control result.");
2495
- }
2496
- handle.io.write(`\nUpdated ${adapterLabel(binding.adapterId)} configuration.\n`);
2497
- }
2498
- }
2499
- if (paneRunning) {
2500
- const answer = (await handle.io.question("Operator is running. Switch session? [y/N]: "))?.trim().toLowerCase();
2501
- if (answer !== "y" && answer !== "yes") {
2502
- process.stdout.write("Cancelled.\n");
2503
- return;
2504
- }
2505
- }
2506
- }
2507
- finally {
2508
- handle.close();
2509
- }
2510
- await prepareInteractiveCodexApp(control.targetAgentId, store, home);
2511
- await ensureFileTaskController(home, { environment: process.env });
2512
- if (paneRunning
2513
- || (active !== undefined
2514
- && active.status === "active")
2515
- || (sessionSet?.providerBinding?.run != null
2516
- && ["submitting", "accepted", "delivery-unknown"].includes(sessionSet.providerBinding.run.status))
2517
- || (sessionSet?.providerBinding?.goal != null && sessionSet.providerBinding.goal.status !== "complete")) {
2518
- await runtime.stopGlobalRoleSession(role.name);
2519
- }
2520
- applyOperatorSessionControl(control, store);
2521
- await runtime.prepareGlobalRoleEnter(role.name);
2522
- tmux.attachRole("operator", role.name, "auto");
2523
- }
2524
- async function prepareInteractiveCodexApp(agentId, store, home) {
2525
- await prepareCodexForCurrentRoles(store, home, agentId);
2526
- }
2527
- async function prepareCodexForCurrentRoles(store, home, targetAgentId) {
2528
- try {
2529
- const result = await prepareLocalCodexStartup(store, home, process.env, targetAgentId);
2530
- if (result.app === "restart-required") {
2531
- codexAppNotice("Codex App is already running without the current Yui shared app-server. "
2532
- + "Quit and reopen Codex App to see and continue Yui sessions.", "Reopen Codex App to connect to Yui sessions.");
2533
- }
2534
- }
2535
- catch (error) {
2536
- const detail = describeCliFailure(error, failureContext()).message;
2537
- const permissionDenied = isCodexInspectionPermissionError(error);
2538
- codexAppNotice(`${permissionDenied
2539
- ? "Codex sharing could not be prepared or verified from this process"
2540
- : "Codex shared runtime could not be prepared"}: ${detail}`, permissionDenied
2541
- ? "macOS denied access while preparing Codex sharing; its state is unverified. See the Yui terminal."
2542
- : "Codex shared runtime could not be prepared; see the Yui terminal.");
2543
- }
2544
- }
2545
- function assertHomeStartupAuthority(store) {
2546
- if (process.env.YUI_SESSION_SCOPE === undefined && process.env.YUI_ROLE === undefined
2547
- && process.env.YUI_AGENT_ID === undefined && process.env.YUI_NATIVE_SESSION_ID === undefined)
2548
- return;
2549
- if (process.env.YUI_SESSION_SCOPE !== "global" || process.env.YUI_ROLE !== "operator") {
2550
- throw usageError("Starting Home services requires the user or current Operator.");
2551
- }
2552
- requireManagedGlobalCaller(store, process.env);
2553
- }
2554
- function codexAppNotice(message, notification) {
2555
- writeCliWarning(message);
2556
- if (process.platform !== "darwin")
2557
- return;
2558
- // The native TUI clears terminal output on attach, so keep the actionable
2559
- // warning visible in Notification Center without interrupting Yui startup.
2560
- spawnSync("/usr/bin/osascript", [
2561
- "-e",
2562
- `display notification "${notification}" with title "Yui"`
2563
- ], { stdio: "ignore", timeout: 3_000 });
2564
- }
2565
- function writeCliWarning(message) {
2566
- const safe = sanitizeFailureText(message, failureContext());
2567
- process.stderr.write(`${jsonOutput
2568
- ? JSON.stringify({ ok: false, code: "RUNTIME_ERROR", message: safe, details: { advisory: true } })
2569
- : `Warning: ${safe}`}\n`);
2570
- }
2571
- function renderControllerResult(method, value) {
2572
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
2573
- return JSON.stringify(value);
2574
- }
2575
- const result = value;
2576
- if (method === "restart") {
2577
- const previousPid = Number.isSafeInteger(result.previousPid) ? String(result.previousPid) : undefined;
2578
- const pid = Number.isSafeInteger(result.pid) ? String(result.pid) : undefined;
2579
- if (previousPid !== undefined && pid !== undefined) {
2580
- return `Controller restarted (PID ${previousPid} -> ${pid}). tmux sessions were not stopped.`;
2581
- }
2582
- return pid === undefined
2583
- ? "Controller restarted. tmux sessions were not stopped."
2584
- : `Controller started (PID ${pid}). tmux sessions were not stopped.`;
2585
- }
2586
- return result.stopped === true
2587
- ? "Controller stopped."
2588
- : "Controller was already stopped.";
2589
- }
2590
- async function completionCommand(home, node) {
2591
- if (args[2] === "candidates") {
2592
- const separator = args.indexOf("--", 4);
2593
- const prefix = args[3];
2594
- if (prefix === undefined || separator !== 4) {
2595
- throw usageError("Completion candidates usage: yui config completion candidates <prefix> -- <words...>");
2596
- }
2597
- const candidates = await resolveCompletionCandidates({
2598
- current: prefix,
2599
- words: args.slice(separator + 1),
2600
- ports: completionSelectionPorts(home)
2601
- });
2602
- process.stdout.write(candidates.length === 0 ? "" : `${candidates.join("\n")}\n`);
2603
- return;
2604
- }
2605
- if (jsonOutput)
2606
- throw usageError("Completion configuration does not support --json.");
2607
- if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
2608
- throw usageError("Completion configuration requires an interactive terminal.");
2609
- }
2610
- const shell = completionShell(args[2]);
2611
- if (args.length > (shell === undefined ? 2 : 3)) {
2612
- throw usageError("Completion usage: yui config completion [bash|zsh|fish]");
2613
- }
2614
- const store = openCurrentTaskStore(home);
2615
- const ioHandle = terminalIo();
2616
- try {
2617
- const manager = new FileCompletionManager(store, process.env);
2618
- emit(await runCompletionWizard(manager, ioHandle.io, shell === undefined ? {} : { shell }));
2619
- }
2620
- finally {
2621
- ioHandle.close();
2622
- }
2623
- void node;
2624
- }
2625
- function completionShell(value) {
2626
- if (value === undefined)
2627
- return undefined;
2628
- if (value === "bash" || value === "zsh" || value === "fish")
2629
- return value;
2630
- throw usageError("Completion shell must be one of bash, zsh, fish.");
2631
- }
2632
- async function resolveTerminalArguments(commandArgs, node, store, catalogs) {
2633
- const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
2634
- if (!interactive || !allowsInteractiveSelection(commandArgs, jsonOutput)) {
2635
- return [...commandArgs];
2636
- }
2637
- const handle = terminalIo();
2638
- try {
2639
- const ports = selectionPorts(store, catalogs);
2640
- const operatorWizard = await resolveOperatorWizardArguments(commandArgs, store.getGlobalRole("operator"), listOperatorSessions(store.getGlobalRoleSessionSet("operator")), handle.io);
2641
- if (operatorWizard.kind === "cancelled")
2642
- return null;
2643
- const operatorArgs = operatorWizard.args;
2644
- // Global Role add owns its Agent choice so the configured default can be
2645
- // shown explicitly. Other commands first resolve missing positional
2646
- // targets through the generic selector, then enter the focused Role UI.
2647
- if ((operatorArgs[0] === "config" && operatorArgs[1] === "role" && operatorArgs[2] === "add")
2648
- || (operatorArgs[0] === "task" && operatorArgs[1] === "role" && operatorArgs[2] === "add")) {
2649
- const wizard = await resolveRoleWizardArguments(operatorArgs, ports, handle.io);
2650
- if (wizard.kind === "cancelled")
2651
- return null;
2652
- const selected = await resolveInteractiveArguments(wizard.args, node, ports, handle.io);
2653
- return selected.kind === "cancelled" ? null : selected.args;
2654
- }
2655
- const selected = await resolveInteractiveArguments(operatorArgs, node, ports, handle.io);
2656
- if (selected.kind === "cancelled")
2657
- return null;
2658
- const roleWizard = await resolveRoleWizardArguments(selected.args, ports, handle.io);
2659
- return roleWizard.kind === "cancelled" ? null : roleWizard.args;
2660
- }
2661
- finally {
2662
- // The Agent process must be the only reader of stdin after Role enter.
2663
- handle.close();
2664
- }
2665
- }
2666
- function terminalIo() {
2667
- const readline = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
2668
- return {
2669
- io: {
2670
- interactive: true,
2671
- json: jsonOutput,
2672
- width: process.stdout.columns ?? 100,
2673
- write: (value) => { process.stdout.write(value); },
2674
- question: async (prompt) => {
2675
- try {
2676
- return await readline.question(prompt);
2677
- }
2678
- catch (error) {
2679
- if (error instanceof Error && (error.name === "AbortError"
2680
- || ("code" in error && error.code === "ERR_USE_AFTER_CLOSE")))
2681
- return undefined;
2682
- throw error;
2683
- }
2684
- }
2685
- },
2686
- close: () => { readline.close(); }
2687
- };
2688
- }
2689
- function selectionPorts(store, catalogs) {
2690
- return {
2691
- call: (method, params) => selectionCall(store, catalogs, method, params)
2692
- };
2693
- }
2694
- async function preflightAgentConfigurationMutation(commandArgs, store, catalogs) {
2695
- const mutation = profileAgentConfigurationMutation(commandArgs, store)
2696
- ?? taskRoleAgentConfigurationMutation(commandArgs, store);
2697
- if (mutation === undefined) {
2698
- await warmConfigurationMutationCatalog(commandArgs, store, catalogs);
2699
- return undefined;
2700
- }
2701
- const agent = store.getConfiguredAgent(mutation.agentId);
2702
- if (agent === null)
2703
- throw agentNotFound(mutation.agentId);
2704
- const resolved = await catalogs.resolve({
2705
- agent,
2706
- cwd: mutation.cwd,
2707
- config: mutation.config
2708
- });
2709
- try {
2710
- validateAgentLaunchConfiguration(resolved.catalog, mutation.config);
2711
- }
2712
- catch (error) {
2713
- throw usageError(error instanceof Error ? error.message : String(error));
2714
- }
2715
- return (candidate) => {
2716
- if (candidate.agentId !== mutation.agentId
2717
- || resolve(candidate.cwd) !== resolve(mutation.cwd)
2718
- || !isDeepStrictEqual(candidate.config, mutation.config)) {
2719
- throw usageError("Agent configuration changed after capability preflight; retry the command.");
2720
- }
2721
- try {
2722
- validateAgentLaunchConfiguration(resolved.catalog, candidate.config);
2723
- }
2724
- catch (error) {
2725
- throw usageError(error instanceof Error ? error.message : String(error));
2726
- }
2727
- };
2728
- }
2729
- function profileAgentConfigurationMutation(args, store) {
2730
- return args[0] === "config" && args[1] === "profile"
2731
- ? previewProfileAgentConfigurationMutation(args.slice(2), store)
2732
- : undefined;
2733
- }
2734
- function taskRoleAgentConfigurationMutation(args, store) {
2735
- return args[0] === "task"
2736
- ? previewTaskRoleAgentConfigurationMutation(args.slice(1), store)
2737
- : undefined;
2738
- }
2739
- async function warmConfigurationMutationCatalog(commandArgs, store, catalogs) {
2740
- if (!hasModelOrEffortMutation(commandArgs))
2741
- return;
2742
- const agentId = configurationMutationAgentId(commandArgs, store);
2743
- if (agentId === undefined)
2744
- return;
2745
- const agent = store.getConfiguredAgent(agentId);
2746
- if (agent === null)
2747
- return;
2748
- await catalogs.resolve({
2749
- agent,
2750
- cwd: store.getConfig().defaultWorkspace ?? process.cwd()
2751
- });
2752
- }
2753
- /**
2754
- * Ask the live Agent Host what the Session is running under, for a Session
2755
- * inspect.
2756
- *
2757
- * Only for `task role session inspect`. Every other command reads persisted
2758
- * state and must keep doing so: a query that reached into a running Session
2759
- * would make reading a Task's records depend on whether an Agent happened to be
2760
- * up.
2761
- *
2762
- * The request is the Host's existing `status` control, which reads state the
2763
- * Host already holds. It creates no Session, sends no prompt and changes nothing
2764
- * — which is what lets a read-only inspect use it at all.
2765
- *
2766
- * Every failure resolves to `unknown` with the reason rather than propagating:
2767
- * no Host is running for most Sessions ever inspected, and the persisted facts
2768
- * the command prints are worth showing regardless of whether a live one answered.
2769
- */
2770
- async function liveHostObservationsForTaskCommand(args, store, home) {
2771
- if (args[0] !== "task" || args[1] !== "role")
2772
- return undefined;
2773
- const inspect = args[2] === "session" && args[3] === "inspect" && args.length >= 6;
2774
- const status = args[2] === "status" && args.length >= 5;
2775
- if (!inspect && !status)
2776
- return undefined;
2777
- const taskId = args[inspect ? 4 : 3];
2778
- const roles = [args[inspect ? 5 : 4]];
2779
- const entries = await Promise.all(roles.map(async (roleName) => [
2780
- roleName, await readLiveHostObservation(store, home, taskId, roleName)
2781
- ]));
2782
- return Object.fromEntries(entries.filter((entry) => entry[1] !== undefined));
2783
- }
2784
- async function readLiveHostObservation(store, home, taskId, roleName) {
2785
- const sessions = store.getTaskRoleSessionSet(taskId, roleName);
2786
- const active = sessions === null
2787
- ? undefined
2788
- : sessions.sessions[sessions.activeAgentId];
2789
- if (active === undefined)
2790
- return undefined;
2791
- if (active.status !== "active") {
2792
- return { detail: `This Session is ${active.status}; no live Host reading was requested.` };
2793
- }
2794
- try {
2795
- const snapshot = await inspectAgentHost({
2796
- home,
2797
- scope: "task",
2798
- taskId,
2799
- roleName
2800
- });
2801
- if (snapshot.nativeSessionId !== active.nativeSessionId
2802
- || snapshot.adapterId !== active.adapterId) {
2803
- return { detail: "The Agent Host does not match the recorded Session." };
2804
- }
2805
- return { snapshot };
2806
- }
2807
26
  catch (error) {
2808
- return { detail: `The Agent Host could not be reached: ${error instanceof Error ? error.message : String(error)}` };
2809
- }
2810
- }
2811
- function runConfigurationForHostObservation(host) {
2812
- if (host === undefined)
2813
- return undefined;
2814
- return host.snapshot?.runConfiguration ?? unknownAgentRunConfiguration(host.detail ?? `Host=${host.snapshot?.state ?? "unknown"}; no live Agent configuration was reported.`);
2815
- }
2816
- function hasModelOrEffortMutation(args) {
2817
- const operation = (args[0] === "config" && args[1] === "role")
2818
- || (args[0] === "task" && args[1] === "role");
2819
- return operation && [
2820
- "--model", "--effort", "--clear-model", "--clear-effort"
2821
- ].some((option) => args.includes(option));
2822
- }
2823
- function configurationMutationAgentId(args, store) {
2824
- const explicit = optionValue(args, "--agent");
2825
- if (explicit !== undefined)
2826
- return explicit;
2827
- if (args[0] === "config" && args[1] === "role" && args[2] === "update") {
2828
- return store.getGlobalRole(args[3] ?? "")?.activeAgentId;
2829
- }
2830
- if (args[0] === "task" && args[1] === "role") {
2831
- if (args[2] === "add")
2832
- return store.getConfig().defaultAgent;
2833
- if (args[2] === "update") {
2834
- return store.getRole(args[3] ?? "", args[4] ?? "")?.activeAgentId;
2835
- }
2836
- }
2837
- return undefined;
2838
- }
2839
- function optionValue(args, option) {
2840
- const index = args.lastIndexOf(option);
2841
- const value = index < 0 ? undefined : args[index + 1];
2842
- return typeof value === "string" && !value.startsWith("--") ? value : undefined;
2843
- }
2844
- function selectionCall(store, catalogs, method, params) {
2845
- const reader = store;
2846
- switch (method) {
2847
- case "agent.list": return store.listConfiguredAgents();
2848
- case "agent.capabilities": {
2849
- const agent = store.getConfiguredAgent(String(params.agentId ?? ""));
2850
- if (agent === null)
2851
- return null;
2852
- const configuredWorkspace = store.getConfig().defaultWorkspace;
2853
- const cwd = typeof params.cwd === "string" && params.cwd.length > 0
2854
- ? params.cwd
2855
- : configuredWorkspace ?? process.cwd();
2856
- const config = typeof params.config === "object" && params.config !== null
2857
- ? params.config
2858
- : undefined;
2859
- return catalogs.resolve({
2860
- agent,
2861
- cwd,
2862
- ...(config === undefined ? {} : { config })
2863
- });
2864
- }
2865
- case "config.get": return store.getConfig();
2866
- case "profile.list": return store.listAgentProfiles().map((profile) => profileSelectionRecord(profile, store));
2867
- case "profile.show": {
2868
- const profile = store.getAgentProfile(String(params.id ?? ""));
2869
- return profile === null ? null : profileSelectionRecord(profile, store);
2870
- }
2871
- case "role.list": return store.listGlobalRoles();
2872
- case "role.show": return store.getGlobalRole(String(params.name ?? ""));
2873
- case "project.list": return callOptional(reader, "listProjects");
2874
- case "task.list": return store.listTaskChoices();
2875
- case "task.integration.list": return store.listIntegrationAttempts(String(params.taskId ?? ""));
2876
- case "task.change-set.list": return store.listChangeSets(String(params.taskId ?? ""));
2877
- case "task.role.list": return callOptional(reader, "listRoles", [params.taskId]);
2878
- case "task.role.show": return callOptional(reader, "getRole", [params.taskId, params.roleName]);
2879
- case "task.work.list": return callOptional(reader, "listWorkItems", [params.taskId]);
2880
- case "task.message.list": return callOptional(reader, "listMessages", [params.taskId]);
2881
- case "task.input.list": {
2882
- const taskId = typeof params.taskId === "string" ? params.taskId : undefined;
2883
- const requests = taskId === undefined
2884
- ? store.listAllInputRequests()
2885
- : store.listInputRequests(taskId);
2886
- return params.all === true ? requests : requests.filter((request) => request.status === "open");
2887
- }
2888
- case "task.turn.list": return callOptional(reader, "listRuns", [params.taskId]);
2889
- case "task.decision.list": return callOptional(reader, "listDecisions", [params.taskId]);
2890
- case "task.milestone.list": return presentSelectionTimes(callOptional(reader, "listMilestones", [params.taskId]), store);
2891
- case "task.event.list": return presentSelectionTimes(callOptional(reader, "listEvents", [params.taskId]), store);
2892
- case "jobs.list": return callOptional(reader, "listJobs");
2893
- default: return [];
2894
- }
2895
- }
2896
- function profileSelectionRecord(profile, store) {
2897
- const view = resolveAgentProfileView(profile, store);
2898
- return {
2899
- ...view.profile,
2900
- runtimeSource: view.runtime.source,
2901
- ...(view.runtime.source === "global-worker"
2902
- ? { workerRevision: view.runtime.workerRevision }
2903
- : {}),
2904
- effectiveRuntime: view.runtime,
2905
- ...(view.runtime.status === "resolved"
2906
- ? {
2907
- agentId: view.runtime.binding.agentId,
2908
- adapterId: view.runtime.binding.adapterId,
2909
- model: view.runtime.binding.config.model,
2910
- effort: view.runtime.binding.config.effort
2911
- }
2912
- : {})
2913
- };
2914
- }
2915
- function presentSelectionTimes(value, store) {
2916
- if (!Array.isArray(value))
2917
- return value;
2918
- const timeZone = store.getConfig().timeZone;
2919
- return value.map((record) => {
2920
- if (typeof record !== "object" || record === null || Array.isArray(record))
2921
- return record;
2922
- const candidate = record;
2923
- return typeof candidate.createdAt === "string"
2924
- ? {
2925
- ...candidate,
2926
- createdAt: formatTimestamp(candidate.createdAt, timeZone)
2927
- }
2928
- : candidate;
2929
- });
2930
- }
2931
- function callOptional(reader, method, args = []) {
2932
- const operation = reader[method];
2933
- return operation === undefined ? [] : Reflect.apply(operation, reader, args);
2934
- }
2935
- function readableStore(home) {
2936
- return openCurrentTaskStore(home);
2937
- }
2938
- function completionSelectionPorts(home) {
2939
- if (inspectStorageSchema(home).status === "uninitialized") {
2940
- return { call: () => [] };
2941
- }
2942
- const store = readableStore(home);
2943
- return selectionPorts(store, new AgentConfigurationCatalogService(home, { environment: process.env }));
2944
- }
2945
- function emit(output, literal = false, data) {
2946
- const normalized = literal ? output.trimEnd() : output.trimEnd();
2947
- process.stdout.write(`${jsonOutput
2948
- ? JSON.stringify(data === undefined
2949
- ? { ok: true, output: normalized }
2950
- : { ok: true, data })
2951
- : normalized}\n`);
2952
- }
2953
- function emitControlFailure(message, code, details) {
2954
- process.exitCode = 2;
2955
- const context = failureContext();
2956
- const safeMessage = sanitizeFailureText(message.trim(), context);
2957
- const safeDetails = sanitizeFailureDetails({ value: details }, context).value;
2958
- process.stdout.write(`${jsonOutput
2959
- ? JSON.stringify({ ok: false, code, message: safeMessage, details: safeDetails })
2960
- : safeMessage}\n`);
2961
- }
2962
- /** Domain-owned failure reports retain their output envelope and effect receipts. */
2963
- function emitFailureResult(output, data) {
2964
- const context = failureContext();
2965
- emit(sanitizeFailureText(output, context), false, sanitizeFailureDetails({ value: data }, context).value);
2966
- }
2967
- /**
2968
- * The human line for a live steer, corrected to the *actual* live acceptance
2969
- * (decision-3 §7). The store command's `base` line is written optimistically
2970
- * ("Steering …"); only a proven `steered` keeps it. `steer-unknown` (pending) is
2971
- * delivery-unknown, and a rejected/unavailable steer did not deliver — in both
2972
- * cases the Message is retained and the operator must not reissue. There is no
2973
- * retarget, queue, or interrupt fallback here; this only reports.
2974
- */
2975
- function steerReceiptOutput(base, target, messageId, receipt) {
2976
- if (receipt.state === "steered")
2977
- return base;
2978
- const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
2979
- const head = receipt.state === "steer-unknown"
2980
- ? `Steer message ${messageId} to ${target} is delivery-unknown (${receipt.outcome}): the Host `
2981
- + "holds it but the Provider has not yet proven acceptance."
2982
- : `Steer message ${messageId} to ${target} did not deliver (${receipt.outcome}).`;
2983
- return `${head}${detail} The Message is retained; re-read the Session before acting, and do not `
2984
- + "reissue the same input under a new requestId or a different action.\n";
2985
- }
2986
- /**
2987
- * The human line for a live interrupt, corrected to `control.cancellation`
2988
- * (decision-3 §7). Only a proven stop-request keeps the optimistic `base` line.
2989
- * `not-active`/`unknown`/unavailable each report that nothing was proven stopped;
2990
- * no process is ever killed. A then-handoff, if any, was already claimed durably
2991
- * and is delivered once by the ordinary continuation path after a proven terminal.
2992
- */
2993
- function interruptReceiptOutput(base, target, receipt) {
2994
- if (receipt.state === "interrupt-requested")
2995
- return `${base.trim()}\nNative cancel requested; Turn termination is not yet proven.\n`;
2996
- const detail = receipt.detail === undefined ? "" : ` ${receipt.detail}`;
2997
- const reason = receipt.state === "interrupt-not-active"
2998
- ? "found no active Turn to stop (not-active)"
2999
- : receipt.state === "interrupt-unknown"
3000
- ? "could not prove a stop (unknown)"
3001
- : `did not complete (${receipt.outcome})`;
3002
- return `Interrupt of ${target} ${reason}.${detail} No process was killed; re-read the Session `
3003
- + "before retrying.\n";
3004
- }
3005
- function withControllerRefreshWarning(output, refresh, label) {
3006
- if (refresh.status !== "failed")
3007
- return output;
3008
- if (label === "Agent environment") {
3009
- return `${output.trimEnd()}\nWarning: Agent configuration was saved, but its current `
3010
- + `environment values were not applied or persisted (${refresh.message}). Retry the `
3011
- + "Agent command with those variables present, or restart the Controller from an "
3012
- + "environment that provides them.\n";
27
+ process.stderr.write(`${JSON.stringify(failure)}\n`);
28
+ process.exitCode = failure.code === 'agent_config' ? 2 : 1;
3013
29
  }
3014
- return `${output.trimEnd()}\nWarning: ${label} was saved, but the running Controller `
3015
- + `could not be refreshed (${refresh.message}). Restart the Controller to apply it.\n`;
3016
30
  }
3017
- function agentEnvironmentRefreshScope(previous, current, configured) {
3018
- const retainedSources = new Set(configured.flatMap((agent) => (agent.environment.map((binding) => binding.sourceName))));
3019
- const retainedNative = new Set(configured.flatMap((agent) => (nativeAgentEnvironmentNames(agent.adapterId))));
3020
- const currentSources = current?.environment.map((binding) => binding.sourceName) ?? [];
3021
- const previousOnlySources = previous?.environment
3022
- .map((binding) => binding.sourceName)
3023
- .filter((name) => !retainedSources.has(name)) ?? [];
3024
- const currentNative = current === null ? [] : nativeAgentEnvironmentNames(current.adapterId);
3025
- const previousOnlyNative = previous === null
3026
- ? []
3027
- : nativeAgentEnvironmentNames(previous.adapterId).filter((name) => !retainedNative.has(name));
3028
- return {
3029
- sourceNames: [...new Set([...currentSources, ...previousOnlySources])],
3030
- nativeNames: [...new Set([...currentNative, ...previousOnlyNative])]
3031
- };
3032
- }
3033
- function normalizeAliases(input) {
3034
- const normalized = [...input];
3035
- if (normalized.length === 1 && (normalized[0] === "-v" || normalized[0] === "--version")) {
3036
- return ["version"];
3037
- }
3038
- const help = normalized.findIndex((argument) => argument === "-h" || argument === "--help");
3039
- return help === normalized.length - 1 ? ["help", ...normalized.slice(0, help)] : normalized;
31
+ else {
32
+ await import('./controlPlaneCli.js');
3040
33
  }
3041
- export { renderCompletion };
34
+ export {};