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