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